/* Copyright (C) 1989-2022 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 "troff.h"
#include "dictionary.h"
#include "hvunits.h"
#include "stringclass.h"
#include "mtsm.h"
#include "env.h"
#include "request.h"
#include "node.h"
#include "token.h"
#include "div.h"
#include "reg.h"
#include "font.h"
#include "charinfo.h"
#include "macropath.h"
#include "input.h"
#include "defs.h"
#include "unicode.h"
#include "curtime.h"

// Needed for getpid() and isatty()
#include "posix.h"

#include "nonposix.h"

#ifdef NEED_DECLARATION_PUTENV
extern "C" {
  int putenv(const char *);
}
#endif /* NEED_DECLARATION_PUTENV */

#define MACRO_PREFIX "tmac."
#define MACRO_POSTFIX ".tmac"
#define INITIAL_STARTUP_FILE "troffrc"
#define FINAL_STARTUP_FILE   "troffrc-end"
#define DEFAULT_INPUT_STACK_LIMIT 1000

#ifndef DEFAULT_WARNING_MASK
// warnings that are enabled by default
#define DEFAULT_WARNING_MASK \
     (WARN_CHAR|WARN_NUMBER|WARN_BREAK|WARN_SPACE|WARN_FONT|WARN_FILE)
#endif

// initial size of buffer for reading names; expanded as necessary
#define ABUF_SIZE 16

extern "C" const char *program_name;
extern "C" const char *Version_string;

#ifdef COLUMN
void init_column_requests();
#endif /* COLUMN */

static node *read_draw_node();
static void read_color_draw_node(token &);
static void push_token(const token &);
void copy_file();
#ifdef COLUMN
void vjustify();
#endif /* COLUMN */
void transparent_file();

token tok;
int break_flag = 0;
int class_flag = 0;
int color_flag = 1;		// colors are on by default
static int backtrace_flag = 0;
#ifndef POPEN_MISSING
char *pipe_command = 0;
#endif
charinfo *charset_table[256];
unsigned char hpf_code_table[256];

static int warning_mask = DEFAULT_WARNING_MASK;
static int inhibit_errors = 0;
static int ignoring = 0;

static void enable_warning(const char *);
static void disable_warning(const char *);

static int escape_char = '\\';
static symbol end_of_input_macro_name;
static symbol blank_line_macro_name;
static symbol leading_spaces_macro_name;
static int compatible_flag = 0;
static int do_old_compatible_flag = -1;	// for .do request
int ascii_output_flag = 0;
int suppress_output_flag = 0;
int is_html = 0;
int begin_level = 0;		// number of nested \O escapes

int have_input = 0;		// whether \f, \F, \D'F...', \H, \m, \M,
				// \O[345], \R, \s, or \S has been processed
				// in token::next()
int old_have_input = 0;		// value of have_input right before \n
bool device_has_tcommand = false;	// 't' output command supported
int unsafe_flag = 0;		// safer by default

bool have_multiple_params = false;	// e.g., \[e aa], \*[foo bar]

double spread_limit = -3.0 - 1.0;	// negative means deactivated

double warn_scale;
char warn_scaling_indicator;
int debug_state = 0;		// turns on debugging of the html troff state

search_path *mac_path = &safer_macro_path;

// Defaults to the current directory.
search_path include_search_path(0, 0, 0, 1);

static int get_copy(node**, bool = false, bool = false);
static void copy_mode_error(const char *,
			    const errarg & = empty_errarg,
			    const errarg & = empty_errarg,
			    const errarg & = empty_errarg);

enum read_mode { ALLOW_EMPTY, WITH_ARGS, NO_ARGS };
static symbol read_escape_parameter(read_mode = NO_ARGS);
static symbol read_long_escape_parameters(read_mode = NO_ARGS);
static void interpolate_string(symbol);
static void interpolate_string_with_args(symbol);
static void interpolate_macro(symbol, bool = false);
static void interpolate_number_format(symbol);
static void interpolate_environment_variable(symbol);

static symbol composite_glyph_name(symbol);
static void interpolate_arg(symbol);
static request_or_macro *lookup_request(symbol);
static int get_delim_number(units *, unsigned char);
static int get_delim_number(units *, unsigned char, units);
static symbol do_get_long_name(bool, char);
static int get_line_arg(units *res, unsigned char si, charinfo **cp);
static bool read_size(int *);
static symbol get_delim_name();
static void init_registers();
static void trapping_blank_line();

class input_iterator;
input_iterator *make_temp_iterator(const char *);
const char *input_char_description(int);

void process_input_stack();
void chop_macro();	// declare to avoid friend name injection


void set_escape_char()
{
  if (has_arg()) {
    if (tok.ch() == 0) {
      error("cannot select invalid escape character; using '\\'");
      escape_char = '\\';
    }
    else
      escape_char = tok.ch();
  }
  else
    escape_char = '\\';
  skip_line();
}

void escape_off()
{
  escape_char = 0;
  skip_line();
}

static int saved_escape_char = '\\';

void save_escape_char()
{
  saved_escape_char = escape_char;
  skip_line();
}

void restore_escape_char()
{
  escape_char = saved_escape_char;
  skip_line();
}

struct arg_list;

class input_iterator {
public:
  input_iterator();
  input_iterator(int is_div);
  virtual ~input_iterator() {}
  int get(node **);
  friend class input_stack;
  int is_diversion;
  statem *diversion_state;
protected:
  const unsigned char *ptr;
  const unsigned char *eptr;
  input_iterator *next;
private:
  virtual int fill(node **);
  virtual int peek();
  virtual int has_args() { return 0; }
  virtual int nargs() { return 0; }
  virtual input_iterator *get_arg(int) { return 0; }
  virtual arg_list *get_arg_list() { return 0; }
  virtual symbol get_macro_name() { return NULL_SYMBOL; }
  virtual int space_follows_arg(int) { return 0; }
  virtual int get_break_flag() { return 0; }
  virtual int get_location(int, const char **, int *) { return 0; }
  virtual void backtrace() {}
  virtual int set_location(const char *, int) { return 0; }
  virtual int next_file(FILE *, const char *) { return 0; }
  virtual void shift(int) {}
  virtual int is_boundary() {return 0; }
  virtual int is_file() { return 0; }
  virtual int is_macro() { return 0; }
  virtual void save_compatible_flag(int) {}
  virtual int get_compatible_flag() { return 0; }
};

input_iterator::input_iterator()
: is_diversion(0), ptr(0), eptr(0)
{
}

input_iterator::input_iterator(int is_div)
: is_diversion(is_div), ptr(0), eptr(0)
{
}

int input_iterator::fill(node **)
{
  return EOF;
}

int input_iterator::peek()
{
  return EOF;
}

inline int input_iterator::get(node **p)
{
  return ptr < eptr ? *ptr++ : fill(p);
}

class input_boundary : public input_iterator {
public:
  int is_boundary() { return 1; }
};

class input_return_boundary : public input_iterator {
public:
  int is_boundary() { return 2; }
};

class file_iterator : public input_iterator {
  FILE *fp;
  int lineno;
  const char *filename;
  int popened;
  int newline_flag;
  int seen_escape;
  enum { BUF_SIZE = 512 };
  unsigned char buf[BUF_SIZE];
  void close();
public:
  file_iterator(FILE *, const char *, int = 0);
  ~file_iterator();
  int fill(node **);
  int peek();
  int get_location(int, const char **, int *);
  void backtrace();
  int set_location(const char *, int);
  int next_file(FILE *, const char *);
  int is_file();
};

file_iterator::file_iterator(FILE *f, const char *fn, int po)
: fp(f), lineno(1), filename(fn), popened(po),
  newline_flag(0), seen_escape(0)
{
  if ((font::use_charnames_in_special) && (fn != 0)) {
    if (!the_output)
      init_output();
    the_output->put_filename(fn, po);
  }
}

file_iterator::~file_iterator()
{
  close();
}

void file_iterator::close()
{
  if (fp == stdin)
    clearerr(stdin);
#ifndef POPEN_MISSING
  else if (popened)
    pclose(fp);
#endif /* not POPEN_MISSING */
  else
    fclose(fp);
}

int file_iterator::is_file()
{
  return 1;
}

int file_iterator::next_file(FILE *f, const char *s)
{
  close();
  filename = s;
  fp = f;
  lineno = 1;
  newline_flag = 0;
  seen_escape = 0;
  popened = 0;
  ptr = 0;
  eptr = 0;
  return 1;
}

int file_iterator::fill(node **)
{
  if (newline_flag)
    lineno++;
  newline_flag = 0;
  unsigned char *p = buf;
  ptr = p;
  unsigned char *e = p + BUF_SIZE;
  while (p < e) {
    int c = getc(fp);
    if (c == EOF)
      break;
    if (is_invalid_input_char(c))
      warning(WARN_INPUT, "invalid input character code %1", int(c));
    else {
      *p++ = c;
      if (c == '\n') {
	seen_escape = 0;
	newline_flag = 1;
	break;
      }
      seen_escape = (c == '\\');
    }
  }
  if (p > buf) {
    eptr = p;
    return *ptr++;
  }
  else {
    eptr = p;
    return EOF;
  }
}

int file_iterator::peek()
{
  int c = getc(fp);
  while (is_invalid_input_char(c)) {
    warning(WARN_INPUT, "invalid input character code %1", int(c));
    c = getc(fp);
  }
  if (c != EOF)
    ungetc(c, fp);
  return c;
}

int file_iterator::get_location(int /*allow_macro*/,
				const char **filenamep, int *linenop)
{
  *linenop = lineno;
  if (filename != 0 && strcmp(filename, "-") == 0)
    *filenamep = "<standard input>";
  else
    *filenamep = filename;
  return 1;
}

void file_iterator::backtrace()
{
  const char *f;
  int n;
  // Get side effect of filename rewrite if stdin.
  (void) get_location(0, &f, &n);
  if (program_name)
    fprintf(stderr, "%s: ", program_name);
  errprint("backtrace: %3 '%1':%2\n", f, n, popened ? "pipe" : "file");
}

int file_iterator::set_location(const char *f, int ln)
{
  if (f) {
    filename = f;
    if (!the_output)
      init_output();
    the_output->put_filename(f, 0);
  }
  lineno = ln;
  return 1;
}

input_iterator nil_iterator;

class input_stack {
public:
  static int get(node **);
  static int peek();
  static void push(input_iterator *);
  static input_iterator *get_arg(int);
  static arg_list *get_arg_list();
  static symbol get_macro_name();
  static int space_follows_arg(int);
  static int get_break_flag();
  static int nargs();
  static int get_location(int, const char **, int *);
  static int set_location(const char *, int);
  static void backtrace();
  static void next_file(FILE *, const char *);
  static void end_file();
  static void shift(int n);
  static void add_boundary();
  static void add_return_boundary();
  static int is_return_boundary();
  static void remove_boundary();
  static int get_level();
  static int get_div_level();
  static void increase_level();
  static void decrease_level();
  static void clear();
  static void pop_macro();
  static void save_compatible_flag(int);
  static int get_compatible_flag();
  static statem *get_diversion_state();
  static void check_end_diversion(input_iterator *t);
  static int limit;
  static int div_level;
  static statem *diversion_state;
private:
  static input_iterator *top;
  static int level;
  static int finish_get(node **);
  static int finish_peek();
};

input_iterator *input_stack::top = &nil_iterator;
int input_stack::level = 0;
int input_stack::limit = DEFAULT_INPUT_STACK_LIMIT;
int input_stack::div_level = 0;
statem *input_stack::diversion_state = 0;
int suppress_push=0;


inline int input_stack::get_level()
{
  return level;
}

inline void input_stack::increase_level()
{
  level++;
}

inline void input_stack::decrease_level()
{
  level--;
}

inline int input_stack::get_div_level()
{
  return div_level;
}

inline int input_stack::get(node **np)
{
  int res = (top->ptr < top->eptr) ? *top->ptr++ : finish_get(np);
  if (res == '\n') {
    old_have_input = have_input;
    have_input = 0;
  }
  return res;
}

int input_stack::finish_get(node **np)
{
  for (;;) {
    int c = top->fill(np);
    if (c != EOF || top->is_boundary())
      return c;
    if (top == &nil_iterator)
      break;
    input_iterator *tem = top;
    check_end_diversion(tem);
#if defined(DEBUGGING)
    if (debug_state)
      if (tem->is_diversion)
	fprintf(stderr,
		"in diversion level = %d\n", input_stack::get_div_level());
#endif
    top = top->next;
    level--;
    delete tem;
    if (top->ptr < top->eptr)
      return *top->ptr++;
  }
  assert(level == 0);
  return EOF;
}

inline int input_stack::peek()
{
  return (top->ptr < top->eptr) ? *top->ptr : finish_peek();
}

void input_stack::check_end_diversion(input_iterator *t)
{
  if (t->is_diversion) {
    div_level--;
    if (diversion_state)
      delete diversion_state;
    diversion_state = t->diversion_state;
  }
}

int input_stack::finish_peek()
{
  for (;;) {
    int c = top->peek();
    if (c != EOF || top->is_boundary())
      return c;
    if (top == &nil_iterator)
      break;
    input_iterator *tem = top;
    check_end_diversion(tem);
    top = top->next;
    level--;
    delete tem;
    if (top->ptr < top->eptr)
      return *top->ptr;
  }
  assert(level == 0);
  return EOF;
}

void input_stack::add_boundary()
{
  push(new input_boundary);
}

void input_stack::add_return_boundary()
{
  push(new input_return_boundary);
}

int input_stack::is_return_boundary()
{
  return top->is_boundary() == 2;
}

void input_stack::remove_boundary()
{
  assert(top->is_boundary());
  input_iterator *temp = top->next;
  check_end_diversion(top);

  delete top;
  top = temp;
  level--;
}

void input_stack::push(input_iterator *in)
{
  if (in == 0)
    return;
  if (++level > limit && limit > 0)
    fatal("input stack limit exceeded (probable infinite loop)");
  in->next = top;
  top = in;
  if (top->is_diversion) {
    div_level++;
    in->diversion_state = diversion_state;
    diversion_state = curenv->construct_state(0);
#if defined(DEBUGGING)
    if (debug_state) {
      curenv->dump_troff_state();
      fflush(stderr);
    }
#endif
  }
#if defined(DEBUGGING)
  if (debug_state)
    if (top->is_diversion) {
      fprintf(stderr,
	      "in diversion level = %d\n", input_stack::get_div_level());
      fflush(stderr);
    }
#endif
}

statem *get_diversion_state()
{
  return input_stack::get_diversion_state();
}

statem *input_stack::get_diversion_state()
{
  if (0 == diversion_state)
    return 0;
  else
    return new statem(diversion_state);
}

input_iterator *input_stack::get_arg(int i)
{
  input_iterator *p;
  for (p = top; p != 0; p = p->next)
    if (p->has_args())
      return p->get_arg(i);
  return 0;
}

arg_list *input_stack::get_arg_list()
{
  input_iterator *p;
  for (p = top; p != 0; p = p->next)
    if (p->has_args())
      return p->get_arg_list();
  return 0;
}

symbol input_stack::get_macro_name()
{
  input_iterator *p;
  for (p = top; p != 0; p = p->next)
    if (p->has_args())
      return p->get_macro_name();
  return NULL_SYMBOL;
}

int input_stack::space_follows_arg(int i)
{
  input_iterator *p;
  for (p = top; p != 0; p = p->next)
    if (p->has_args())
      return p->space_follows_arg(i);
  return 0;
}

int input_stack::get_break_flag()
{
  return top->get_break_flag();
}

void input_stack::shift(int n)
{
  for (input_iterator *p = top; p; p = p->next)
    if (p->has_args()) {
      p->shift(n);
      return;
    }
}

int input_stack::nargs()
{
  for (input_iterator *p =top; p != 0; p = p->next)
    if (p->has_args())
      return p->nargs();
  return 0;
}

int input_stack::get_location(int allow_macro, const char **filenamep, int *linenop)
{
  for (input_iterator *p = top; p; p = p->next)
    if (p->get_location(allow_macro, filenamep, linenop))
      return 1;
  return 0;
}

void input_stack::backtrace()
{
  for (input_iterator *p = top; p; p = p->next)
    p->backtrace();
}

int input_stack::set_location(const char *filename, int lineno)
{
  for (input_iterator *p = top; p; p = p->next)
    if (p->set_location(filename, lineno))
      return 1;
  return 0;
}

void input_stack::next_file(FILE *fp, const char *s)
{
  input_iterator **pp;
  for (pp = &top; *pp != &nil_iterator; pp = &(*pp)->next)
    if ((*pp)->next_file(fp, s))
      return;
  if (++level > limit && limit > 0)
    fatal("input stack limit exceeded");
  *pp = new file_iterator(fp, s);
  (*pp)->next = &nil_iterator;
}

void input_stack::end_file()
{
  for (input_iterator **pp = &top; *pp != &nil_iterator; pp = &(*pp)->next)
    if ((*pp)->is_file()) {
      input_iterator *tem = *pp;
      check_end_diversion(tem);
      *pp = (*pp)->next;
      delete tem;
      level--;
      return;
    }
}

void input_stack::clear()
{
  int nboundaries = 0;
  while (top != &nil_iterator) {
    if (top->is_boundary())
      nboundaries++;
    input_iterator *tem = top;
    check_end_diversion(tem);
    top = top->next;
    level--;
    delete tem;
  }
  // Keep while_request happy.
  for (; nboundaries > 0; --nboundaries)
    add_return_boundary();
}

void input_stack::pop_macro()
{
  int nboundaries = 0;
  int is_macro = 0;
  do {
    if (top->next == &nil_iterator)
      break;
    if (top->is_boundary())
      nboundaries++;
    is_macro = top->is_macro();
    input_iterator *tem = top;
    check_end_diversion(tem);
    top = top->next;
    level--;
    delete tem;
  } while (!is_macro);
  // Keep while_request happy.
  for (; nboundaries > 0; --nboundaries)
    add_return_boundary();
}

inline void input_stack::save_compatible_flag(int f)
{
  top->save_compatible_flag(f);
}

inline int input_stack::get_compatible_flag()
{
  return top->get_compatible_flag();
}

void backtrace_request()
{
  input_stack::backtrace();
  fflush(stderr);
  skip_line();
}

void next_file()
{
  symbol nm = get_long_name();
  while (!tok.is_newline() && !tok.is_eof())
    tok.next();
  if (nm.is_null())
    input_stack::end_file();
  else {
    errno = 0;
    FILE *fp = include_search_path.open_file_cautious(nm.contents());
    if (!fp)
      error("can't open '%1': %2", nm.contents(), strerror(errno));
    else
      input_stack::next_file(fp, nm.contents());
  }
  tok.next();
}

void shift()
{
  int n;
  if (!has_arg() || !get_integer(&n))
    n = 1;
  input_stack::shift(n);
  skip_line();
}

static char get_char_for_escape_parameter(bool allow_space = false)
{
  int c = get_copy(0 /* nullptr */, false /* is defining */,
		   true /* handle \E */);
  switch (c) {
  case EOF:
    copy_mode_error("end of input in escape sequence");
    return '\0';
  default:
    if (!is_invalid_input_char(c))
      break;
    // fall through
  case '\n':
    if (c == '\n')
      input_stack::push(make_temp_iterator("\n"));
    // fall through
  case ' ':
    if (c == ' ' && allow_space)
      break;
    // fall through
  case '\t':
  case '\001':
  case '\b':
    copy_mode_error("%1 is not allowed in an escape sequence parameter",
		    input_char_description(c));
    return '\0';
  }
  return c;
}

static symbol read_two_char_escape_parameter()
{
  char buf[3];
  buf[0] = get_char_for_escape_parameter();
  if (buf[0] != '\0') {
    buf[1] = get_char_for_escape_parameter();
    if (buf[1] == '\0')
      buf[0] = 0;
    else
      buf[2] = 0;
  }
  return symbol(buf);
}

static symbol read_long_escape_parameters(read_mode mode)
{
  int start_level = input_stack::get_level();
  char abuf[ABUF_SIZE];
  char *buf = abuf;
  int buf_size = ABUF_SIZE;
  int i = 0;
  char c;
  int have_char = 0;
  for (;;) {
    c = get_char_for_escape_parameter(have_char && mode == WITH_ARGS);
    if (c == 0) {
      if (buf != abuf)
	delete[] buf;
      return NULL_SYMBOL;
    }
    have_char = 1;
    if (mode == WITH_ARGS && c == ' ')
      break;
    if (i + 2 > buf_size) {
      if (buf == abuf) {
	buf = new char[ABUF_SIZE*2];
	memcpy(buf, abuf, buf_size);
	buf_size = ABUF_SIZE*2;
      }
      else {
	char *old_buf = buf;
	buf = new char[buf_size*2];
	memcpy(buf, old_buf, buf_size);
	buf_size *= 2;
	delete[] old_buf;
      }
    }
    if (c == ']' && input_stack::get_level() == start_level)
      break;
    buf[i++] = c;
  }
  buf[i] = 0;
  if (c == ' ')
    have_multiple_params = true;
  if (buf == abuf) {
    if (i == 0) {
      if (mode != ALLOW_EMPTY)
	copy_mode_error("empty escape name");
      return EMPTY_SYMBOL;
    }
    return symbol(abuf);
  }
  else {
    symbol s(buf);
    delete[] buf;
    return s;
  }
}

static symbol read_escape_parameter(read_mode mode)
{
  char c = get_char_for_escape_parameter();
  if (c == 0)
    return NULL_SYMBOL;
  if (c == '(')
    return read_two_char_escape_parameter();
  if (c == '[' && !compatible_flag)
    return read_long_escape_parameters(mode);
  char buf[2];
  buf[0] = c;
  buf[1] = '\0';
  return symbol(buf);
}

static symbol read_increment_and_escape_parameter(int *incp)
{
  char c = get_char_for_escape_parameter();
  switch (c) {
  case 0:
    *incp = 0;
    return NULL_SYMBOL;
  case '(':
    *incp = 0;
    return read_two_char_escape_parameter();
  case '+':
    *incp = 1;
    return read_escape_parameter();
  case '-':
    *incp = -1;
    return read_escape_parameter();
  case '[':
    if (!compatible_flag) {
      *incp = 0;
      return read_long_escape_parameters();
    }
    break;
  }
  *incp = 0;
  char buf[2];
  buf[0] = c;
  buf[1] = '\0';
  return symbol(buf);
}

static int get_copy(node **nd, bool is_defining, bool handle_escape_E)
{
  for (;;) {
    int c = input_stack::get(nd);
    if (c == PUSH_GROFF_MODE) {
      input_stack::save_compatible_flag(compatible_flag);
      compatible_flag = 0;
      continue;
    }
    if (c == PUSH_COMP_MODE) {
      input_stack::save_compatible_flag(compatible_flag);
      compatible_flag = 1;
      continue;
    }
    if (c == POP_GROFFCOMP_MODE) {
      compatible_flag = input_stack::get_compatible_flag();
      continue;
    }
    if (c == BEGIN_QUOTE) {
      input_stack::increase_level();
      continue;
    }
    if (c == END_QUOTE) {
      input_stack::decrease_level();
      continue;
    }
    if (c == DOUBLE_QUOTE)
      continue;
    if (c == ESCAPE_E && handle_escape_E)
      c = escape_char;
    if (c == ESCAPE_NEWLINE) {
      if (is_defining)
	return c;
      do {
	c = input_stack::get(nd);
      } while (c == ESCAPE_NEWLINE);
    }
    if (c != escape_char || escape_char <= 0)
      return c;
  again:
    c = input_stack::peek();
    switch(c) {
    case 0:
      return escape_char;
    case '"':
      (void)input_stack::get(0);
      while ((c = input_stack::get(0)) != '\n' && c != EOF)
	;
      return c;
    case '#':			// Like \" but newline is ignored.
      (void)input_stack::get(0);
      while ((c = input_stack::get(0)) != '\n')
	if (c == EOF)
	  return EOF;
      break;
    case '$':
      {
	(void)input_stack::get(0);
	symbol s = read_escape_parameter();
	if (!(s.is_null() || s.is_empty()))
	  interpolate_arg(s);
	break;
      }
    case '*':
      {
	(void)input_stack::get(0);
	symbol s = read_escape_parameter(WITH_ARGS);
	if (!(s.is_null() || s.is_empty())) {
	  if (have_multiple_params) {
	    have_multiple_params = false;
	    interpolate_string_with_args(s);
	  }
	  else
	    interpolate_string(s);
	}
	break;
      }
    case 'a':
      (void)input_stack::get(0);
      return '\001';
    case 'e':
      (void)input_stack::get(0);
      return ESCAPE_e;
    case 'E':
      (void)input_stack::get(0);
      if (handle_escape_E)
	goto again;
      return ESCAPE_E;
    case 'n':
      {
	(void)input_stack::get(0);
	int inc;
	symbol s = read_increment_and_escape_parameter(&inc);
	if (!(s.is_null() || s.is_empty()))
	  interpolate_number_reg(s, inc);
	break;
      }
    case 'g':
      {
	(void)input_stack::get(0);
	symbol s = read_escape_parameter();
	if (!(s.is_null() || s.is_empty()))
	  interpolate_number_format(s);
	break;
      }
    case 't':
      (void)input_stack::get(0);
      return '\t';
    case 'V':
      {
	(void)input_stack::get(0);
	symbol s = read_escape_parameter();
	if (!(s.is_null() || s.is_empty()))
	  interpolate_environment_variable(s);
	break;
      }
    case '\n':
      (void)input_stack::get(0);
      if (is_defining)
	return ESCAPE_NEWLINE;
      break;
    case ' ':
      (void)input_stack::get(0);
      return ESCAPE_SPACE;
    case '~':
      (void)input_stack::get(0);
      return ESCAPE_TILDE;
    case ':':
      (void)input_stack::get(0);
      return ESCAPE_COLON;
    case '|':
      (void)input_stack::get(0);
      return ESCAPE_BAR;
    case '^':
      (void)input_stack::get(0);
      return ESCAPE_CIRCUMFLEX;
    case '{':
      (void)input_stack::get(0);
      return ESCAPE_LEFT_BRACE;
    case '}':
      (void)input_stack::get(0);
      return ESCAPE_RIGHT_BRACE;
    case '`':
      (void)input_stack::get(0);
      return ESCAPE_LEFT_QUOTE;
    case '\'':
      (void)input_stack::get(0);
      return ESCAPE_RIGHT_QUOTE;
    case '-':
      (void)input_stack::get(0);
      return ESCAPE_HYPHEN;
    case '_':
      (void)input_stack::get(0);
      return ESCAPE_UNDERSCORE;
    case 'c':
      (void)input_stack::get(0);
      return ESCAPE_c;
    case '!':
      (void)input_stack::get(0);
      return ESCAPE_BANG;
    case '?':
      (void)input_stack::get(0);
      return ESCAPE_QUESTION;
    case '&':
      (void)input_stack::get(0);
      return ESCAPE_AMPERSAND;
    case ')':
      (void)input_stack::get(0);
      return ESCAPE_RIGHT_PARENTHESIS;
    case '.':
      (void)input_stack::get(0);
      return c;
    case '%':
      (void)input_stack::get(0);
      return ESCAPE_PERCENT;
    default:
      if (c == escape_char) {
	(void)input_stack::get(0);
	return c;
      }
      else
	return escape_char;
    }
  }
}

class non_interpreted_char_node : public node {
  unsigned char c;
public:
  non_interpreted_char_node(unsigned char);
  node *copy();
  int interpret(macro *);
  int same(node *);
  const char *type();
  int force_tprint();
  int is_tag();
};

int non_interpreted_char_node::same(node *nd)
{
  return c == ((non_interpreted_char_node *)nd)->c;
}

const char *non_interpreted_char_node::type()
{
  return "non_interpreted_char_node";
}

int non_interpreted_char_node::force_tprint()
{
  return 0;
}

int non_interpreted_char_node::is_tag()
{
  return 0;
}

non_interpreted_char_node::non_interpreted_char_node(unsigned char n) : c(n)
{
  assert(n != 0);
}

node *non_interpreted_char_node::copy()
{
  return new non_interpreted_char_node(c);
}

int non_interpreted_char_node::interpret(macro *mac)
{
  mac->append(c);
  return 1;
}

static void do_width();
static node *do_non_interpreted();
static node *do_special();
static node *do_suppress(symbol nm);
static void do_register();

dictionary color_dictionary(501);

static color *lookup_color(symbol nm)
{
  assert(!nm.is_null());
  if (nm == default_symbol)
    return &default_color;
  color *c = (color *)color_dictionary.lookup(nm);
  if (c == 0)
    warning(WARN_COLOR, "color '%1' not defined", nm.contents());
  return c;
}

void do_glyph_color(symbol nm)
{
  if (nm.is_null())
    return;
  if (nm.is_empty())
    curenv->set_glyph_color(curenv->get_prev_glyph_color());
  else {
    color *tem = lookup_color(nm);
    if (tem)
      curenv->set_glyph_color(tem);
    else
      (void)color_dictionary.lookup(nm, new color(nm));
  }
}

void do_fill_color(symbol nm)
{
  if (nm.is_null())
    return;
  if (nm.is_empty())
    curenv->set_fill_color(curenv->get_prev_fill_color());
  else {
    color *tem = lookup_color(nm);
    if (tem)
      curenv->set_fill_color(tem);
    else
      (void)color_dictionary.lookup(nm, new color(nm));
  }
}

static unsigned int get_color_element(const char *scheme, const char *col)
{
  units val;
  if (!get_number(&val, 'f')) {
    warning(WARN_COLOR, "%1 in %2 definition set to 0", col, scheme);
    tok.next();
    return 0;
  }
  if (val < 0) {
    warning(WARN_RANGE, "%1 cannot be negative: set to 0", col);
    return 0;
  }
  if (val > color::MAX_COLOR_VAL+1) {
    warning(WARN_RANGE, "%1 cannot be greater than 1", col);
    // we change 0x10000 to 0xffff
    return color::MAX_COLOR_VAL;
  }
  return (unsigned int)val;
}

static color *read_rgb(char end = 0)
{
  symbol component = do_get_long_name(0, end);
  if (component.is_null()) {
    warning(WARN_COLOR, "missing rgb color values");
    return 0;
  }
  const char *s = component.contents();
  color *col = new color;
  if (*s == '#') {
    if (!col->read_rgb(s)) {
      warning(WARN_COLOR, "expecting rgb color definition,"
	      " not '%1'", s);
      delete col;
      return 0;
    }
  }
  else {
    if (!end)
      input_stack::push(make_temp_iterator(" "));
    input_stack::push(make_temp_iterator(s));
    tok.next();
    unsigned int r = get_color_element("rgb color", "red component");
    unsigned int g = get_color_element("rgb color", "green component");
    unsigned int b = get_color_element("rgb color", "blue component");
    col->set_rgb(r, g, b);
  }
  return col;
}

static color *read_cmy(char end = 0)
{
  symbol component = do_get_long_name(0, end);
  if (component.is_null()) {
    warning(WARN_COLOR, "missing cmy color values");
    return 0;
  }
  const char *s = component.contents();
  color *col = new color;
  if (*s == '#') {
    if (!col->read_cmy(s)) {
      warning(WARN_COLOR, "expecting cmy color definition,"
	      " not '%1'", s);
      delete col;
      return 0;
    }
  }
  else {
    if (!end)
      input_stack::push(make_temp_iterator(" "));
    input_stack::push(make_temp_iterator(s));
    tok.next();
    unsigned int c = get_color_element("cmy color", "cyan component");
    unsigned int m = get_color_element("cmy color", "magenta component");
    unsigned int y = get_color_element("cmy color", "yellow component");
    col->set_cmy(c, m, y);
  }
  return col;
}

static color *read_cmyk(char end = 0)
{
  symbol component = do_get_long_name(0, end);
  if (component.is_null()) {
    warning(WARN_COLOR, "missing cmyk color values");
    return 0;
  }
  const char *s = component.contents();
  color *col = new color;
  if (*s == '#') {
    if (!col->read_cmyk(s)) {
      warning(WARN_COLOR, "expecting cmyk color definition,"
	      " not '%1'", s);
      delete col;
      return 0;
    }
  }
  else {
    if (!end)
      input_stack::push(make_temp_iterator(" "));
    input_stack::push(make_temp_iterator(s));
    tok.next();
    unsigned int c = get_color_element("cmyk color", "cyan component");
    unsigned int m = get_color_element("cmyk color", "magenta component");
    unsigned int y = get_color_element("cmyk color", "yellow component");
    unsigned int k = get_color_element("cmyk color", "black component");
    col->set_cmyk(c, m, y, k);
  }
  return col;
}

static color *read_gray(char end = 0)
{
  symbol component = do_get_long_name(0, end);
  if (component.is_null()) {
    warning(WARN_COLOR, "missing gray value");
    return 0;
  }
  const char *s = component.contents();
  color *col = new color;
  if (*s == '#') {
    if (!col->read_gray(s)) {
      warning(WARN_COLOR, "expecting gray definition,"
	      " not '%1'", s);
      delete col;
      return 0;
    }
  }
  else {
    if (!end)
      input_stack::push(make_temp_iterator("\n"));
    input_stack::push(make_temp_iterator(s));
    tok.next();
    unsigned int g = get_color_element("gray", "gray value");
    col->set_gray(g);
  }
  return col;
}

static void activate_color()
{
  int n;
  if (has_arg() && get_integer(&n))
    color_flag = n != 0;
  else
    color_flag = 1;
  skip_line();
}

static void define_color()
{
  symbol color_name = get_long_name(true /* required */);
  if (color_name.is_null()) {
    skip_line();
    return;
  }
  if (color_name == default_symbol) {
    warning(WARN_COLOR, "default color can't be redefined");
    skip_line();
    return;
  }
  symbol style = get_long_name(true /* required */);
  if (style.is_null()) {
    skip_line();
    return;
  }
  color *col;
  if (strcmp(style.contents(), "rgb") == 0)
    col = read_rgb();
  else if (strcmp(style.contents(), "cmyk") == 0)
    col = read_cmyk();
  else if (strcmp(style.contents(), "gray") == 0)
    col = read_gray();
  else if (strcmp(style.contents(), "grey") == 0)
    col = read_gray();
  else if (strcmp(style.contents(), "cmy") == 0)
    col = read_cmy();
  else {
    warning(WARN_COLOR, "unknown color space '%1';"
	    " use 'rgb', 'cmyk', 'gray' or 'cmy'", style.contents());
    skip_line();
    return;
  }
  if (col) {
    col->nm = color_name;
    (void)color_dictionary.lookup(color_name, col);
  }
  skip_line();
}

node *do_overstrike()
{
  overstrike_node *on = new overstrike_node;
  int start_level = input_stack::get_level();
  token start;
  start.next();
  for (;;) {
    tok.next();
    if (tok.is_newline()) {
      input_stack::push(make_temp_iterator("\n"));
      break;
    }
    if (tok.is_eof()) {
      warning(WARN_DELIM, "missing closing delimiter in overstrike"
	     " escape sequence (got %1)", tok.description());
      // Synthesize an input line ending.
      input_stack::push(make_temp_iterator("\n"));
      break;
    }
    if (tok == start
	&& (compatible_flag || input_stack::get_level() == start_level))
      break;
    if (tok.is_horizontal_space())
      on->overstrike(tok.nd->copy());
    else if (tok.is_unstretchable_space())
    {
      node *n = new hmotion_node(curenv->get_space_width(),
				 curenv->get_fill_color());
      on->overstrike(n);
    }
    else {
      charinfo *ci = tok.get_char(true /* required */);
      if (ci) {
	node *n = curenv->make_char_node(ci);
	if (n)
	  on->overstrike(n);
      }
    }
  }
  return on;
}

static node *do_bracket()
{
  bracket_node *bn = new bracket_node;
  int start_level = input_stack::get_level();
  token start;
  start.next();
  for (;;) {
    tok.next();
    if (tok.is_newline()) {
      input_stack::push(make_temp_iterator("\n"));
      break;
    }
    if (tok.is_eof()) {
      warning(WARN_DELIM, "missing closing delimiter in"
	      " bracket-building escape sequence (got %1)",
	      tok.description());
      // Synthesize an input line ending.
      input_stack::push(make_temp_iterator("\n"));
      break;
    }
    if (tok == start
	&& (compatible_flag || input_stack::get_level() == start_level))
      break;
    charinfo *ci = tok.get_char(true /* required */);
    if (ci) {
      node *n = curenv->make_char_node(ci);
      if (n)
	bn->bracket(n);
    }
  }
  return bn;
}

static int do_name_test()
{
  int start_level = input_stack::get_level();
  token start;
  start.next();
  bool got_bad_char = false;
  bool got_some_char = false;
  for (;;) {
    tok.next();
    if (tok.is_newline() || tok.is_eof()) {
      if (tok != start)
	warning(WARN_DELIM, "missing closing delimiter in identifier"
		" validation escape sequence (got %1)",
		tok.description());
      // Synthesize an input line ending.
      input_stack::push(make_temp_iterator("\n"));
      break;
    }
    if (tok == start
	&& (compatible_flag || input_stack::get_level() == start_level))
      break;
    if (!tok.ch())
      got_bad_char = true;
    got_some_char = true;
  }
  return (got_some_char && !got_bad_char);
}

static int do_expr_test()
{
  token start;
  start.next();
  int start_level = input_stack::get_level();
  if (!start.usable_as_delimiter(true /* report error */))
    return 0;
  tok.next();
  // disable all warning and error messages temporarily
  int saved_warning_mask = warning_mask;
  int saved_inhibit_errors = inhibit_errors;
  warning_mask = 0;
  inhibit_errors = 1;
  int dummy;
  int result = get_number_rigidly(&dummy, 'u');
  warning_mask = saved_warning_mask;
  inhibit_errors = saved_inhibit_errors;
  if (tok == start && input_stack::get_level() == start_level)
    return result;
  // ignore everything up to the delimiter in case we aren't right there
  for (;;) {
    tok.next();
    if (tok.is_newline() || tok.is_eof()) {
      warning(WARN_DELIM, "missing closing delimiter in"
	      " expression test escape sequence (got %1)",
	      tok.description());
      input_stack::push(make_temp_iterator("\n"));
      break;
    }
    if (tok == start && input_stack::get_level() == start_level)
      break;
  }
  return 0;
}

#if 0
static node *do_zero_width()
{
  token start;
  start.next();
  int start_level = input_stack::get_level();
  environment env(curenv);
  environment *oldenv = curenv;
  curenv = &env;
  for (;;) {
    tok.next();
    if (tok.is_newline() || tok.is_eof()) {
      error("missing closing delimiter");
      break;
    }
    if (tok == start
	&& (compatible_flag || input_stack::get_level() == start_level))
      break;
    tok.process();
  }
  curenv = oldenv;
  node *rev = env.extract_output_line();
  node *n = 0;
  while (rev) {
    node *tem = rev;
    rev = rev->next;
    tem->next = n;
    n = tem;
  }
  return new zero_width_node(n);
}

#else

// It's undesirable for \Z to change environments, because then
// \n(.w won't work as expected.

static node *do_zero_width()
{
  node *rev = new dummy_node;
  int start_level = input_stack::get_level();
  token start;
  start.next();
  for (;;) {
    tok.next();
    if (tok.is_newline() || tok.is_eof()) {
      if (tok != start)
	warning(WARN_DELIM, "missing closing delimiter in"
		" zero-width escape sequence (got %1)",
		tok.description());
      // Synthesize an input line ending.
      input_stack::push(make_temp_iterator("\n"));
      break;
    }
    if (tok == start
	&& (compatible_flag || input_stack::get_level() == start_level))
      break;
    if (!tok.add_to_zero_width_node_list(&rev))
      error("invalid token in argument to escaped 'Z'");
  }
  node *n = 0;
  while (rev) {
    node *tem = rev;
    rev = rev->next;
    tem->next = n;
    n = tem;
  }
  return new zero_width_node(n);
}

#endif

token_node *node::get_token_node()
{
  return 0;
}

class token_node : public node {
public:
  token tk;
  token_node(const token &t);
  node *copy();
  token_node *get_token_node();
  int same(node *);
  const char *type();
  int force_tprint();
  int is_tag();
};

token_node::token_node(const token &t) : tk(t)
{
}

node *token_node::copy()
{
  return new token_node(tk);
}

token_node *token_node::get_token_node()
{
  return this;
}

int token_node::same(node *nd)
{
  return tk == ((token_node *)nd)->tk;
}

const char *token_node::type()
{
  return "token_node";
}

int token_node::force_tprint()
{
  return 0;
}

int token_node::is_tag()
{
  return 0;
}

token::token() : nd(0), type(TOKEN_EMPTY)
{
}

token::~token()
{
  delete nd;
}

token::token(const token &t)
: nm(t.nm), c(t.c), val(t.val), dim(t.dim), type(t.type)
{
  // Use two statements to work around bug in SGI C++.
  node *tem = t.nd;
  nd = tem ? tem->copy() : 0;
}

void token::operator=(const token &t)
{
  delete nd;
  nm = t.nm;
  // Use two statements to work around bug in SGI C++.
  node *tem = t.nd;
  nd = tem ? tem->copy() : 0;
  c = t.c;
  val = t.val;
  dim = t.dim;
  type = t.type;
}

void token::skip()
{
  while (is_space())
    next();
}

bool has_arg()
{
  while (tok.is_space())
    tok.next();
  return !tok.is_newline();
}

void token::make_space()
{
  type = TOKEN_SPACE;
}

void token::make_newline()
{
  type = TOKEN_NEWLINE;
}

void token::next()
{
  if (nd) {
    delete nd;
    nd = 0;
  }
  units x;
  for (;;) {
    node *n = 0;
    int cc = input_stack::get(&n);
    if (cc != escape_char || escape_char == 0) {
    handle_ordinary_char:
      switch(cc) {
      case INPUT_NO_BREAK_SPACE:
	  type = TOKEN_STRETCHABLE_SPACE;
	  return;
      case INPUT_SOFT_HYPHEN:
	  type = TOKEN_HYPHEN_INDICATOR;
	  return;
      case PUSH_GROFF_MODE:
	input_stack::save_compatible_flag(compatible_flag);
	compatible_flag = 0;
	continue;
      case PUSH_COMP_MODE:
	input_stack::save_compatible_flag(compatible_flag);
	compatible_flag = 1;
	continue;
      case POP_GROFFCOMP_MODE:
	compatible_flag = input_stack::get_compatible_flag();
	continue;
      case BEGIN_QUOTE:
	input_stack::increase_level();
	continue;
      case END_QUOTE:
	input_stack::decrease_level();
	continue;
      case DOUBLE_QUOTE:
	continue;
      case EOF:
	type = TOKEN_EOF;
	return;
      case TRANSPARENT_FILE_REQUEST:
      case TITLE_REQUEST:
      case COPY_FILE_REQUEST:
#ifdef COLUMN
      case VJUSTIFY_REQUEST:
#endif /* COLUMN */
	type = TOKEN_REQUEST;
	c = cc;
	return;
      case BEGIN_TRAP:
	type = TOKEN_BEGIN_TRAP;
	return;
      case END_TRAP:
	type = TOKEN_END_TRAP;
	return;
      case LAST_PAGE_EJECTOR:
	seen_last_page_ejector = 1;
	// fall through
      case PAGE_EJECTOR:
	type = TOKEN_PAGE_EJECTOR;
	return;
      case ESCAPE_PERCENT:
      ESCAPE_PERCENT:
	type = TOKEN_HYPHEN_INDICATOR;
	return;
      case ESCAPE_SPACE:
      ESCAPE_SPACE:
	type = TOKEN_UNSTRETCHABLE_SPACE;
	return;
      case ESCAPE_TILDE:
      ESCAPE_TILDE:
	type = TOKEN_STRETCHABLE_SPACE;
	return;
      case ESCAPE_COLON:
      ESCAPE_COLON:
	type = TOKEN_ZERO_WIDTH_BREAK;
	return;
      case ESCAPE_e:
      ESCAPE_e:
	type = TOKEN_ESCAPE;
	return;
      case ESCAPE_E:
	goto handle_escape_char;
      case ESCAPE_BAR:
      ESCAPE_BAR:
	type = TOKEN_HORIZONTAL_SPACE;
	nd = new hmotion_node(curenv->get_narrow_space_width(),
			      curenv->get_fill_color());
	return;
      case ESCAPE_CIRCUMFLEX:
      ESCAPE_CIRCUMFLEX:
	type = TOKEN_HORIZONTAL_SPACE;
	nd = new hmotion_node(curenv->get_half_narrow_space_width(),
			      curenv->get_fill_color());
	return;
      case ESCAPE_NEWLINE:
	have_input = 0;
	break;
      case ESCAPE_LEFT_BRACE:
      ESCAPE_LEFT_BRACE:
	type = TOKEN_LEFT_BRACE;
	return;
      case ESCAPE_RIGHT_BRACE:
      ESCAPE_RIGHT_BRACE:
	type = TOKEN_RIGHT_BRACE;
	return;
      case ESCAPE_LEFT_QUOTE:
      ESCAPE_LEFT_QUOTE:
	type = TOKEN_SPECIAL;
	nm = symbol("ga");
	return;
      case ESCAPE_RIGHT_QUOTE:
      ESCAPE_RIGHT_QUOTE:
	type = TOKEN_SPECIAL;
	nm = symbol("aa");
	return;
      case ESCAPE_HYPHEN:
      ESCAPE_HYPHEN:
	type = TOKEN_SPECIAL;
	nm = symbol("-");
	return;
      case ESCAPE_UNDERSCORE:
      ESCAPE_UNDERSCORE:
	type = TOKEN_SPECIAL;
	nm = symbol("ul");
	return;
      case ESCAPE_c:
      ESCAPE_c:
	type = TOKEN_INTERRUPT;
	return;
      case ESCAPE_BANG:
      ESCAPE_BANG:
	type = TOKEN_TRANSPARENT;
	return;
      case ESCAPE_QUESTION:
      ESCAPE_QUESTION:
	nd = do_non_interpreted();
	if (nd) {
	  type = TOKEN_NODE;
	  return;
	}
	break;
      case ESCAPE_AMPERSAND:
      ESCAPE_AMPERSAND:
	type = TOKEN_DUMMY;
	return;
      case ESCAPE_RIGHT_PARENTHESIS:
      ESCAPE_RIGHT_PARENTHESIS:
	type = TOKEN_TRANSPARENT_DUMMY;
	return;
      case '\b':
	type = TOKEN_BACKSPACE;
	return;
      case ' ':
	type = TOKEN_SPACE;
	return;
      case '\t':
	type = TOKEN_TAB;
	return;
      case '\n':
	type = TOKEN_NEWLINE;
	return;
      case '\001':
	type = TOKEN_LEADER;
	return;
      case 0:
	{
	  assert(n != 0);
	  token_node *tn = n->get_token_node();
	  if (tn) {
	    *this = tn->tk;
	    delete tn;
	  }
	  else {
	    nd = n;
	    type = TOKEN_NODE;
	  }
	}
	return;
      default:
	type = TOKEN_CHAR;
	c = cc;
	return;
      }
    }
    else {
    handle_escape_char:
      cc = input_stack::get(&n);
      switch(cc) {
      case '(':
	nm = read_two_char_escape_parameter();
	type = TOKEN_SPECIAL;
	return;
      case EOF:
	type = TOKEN_EOF;
	error("end of input after escape character");
	return;
      case '`':
	goto ESCAPE_LEFT_QUOTE;
      case '\'':
	goto ESCAPE_RIGHT_QUOTE;
      case '-':
	goto ESCAPE_HYPHEN;
      case '_':
	goto ESCAPE_UNDERSCORE;
      case '%':
	goto ESCAPE_PERCENT;
      case ' ':
	goto ESCAPE_SPACE;
      case '0':
	nd = new hmotion_node(curenv->get_digit_width(),
			      curenv->get_fill_color());
	type = TOKEN_HORIZONTAL_SPACE;
	return;
      case '|':
	goto ESCAPE_BAR;
      case '^':
	goto ESCAPE_CIRCUMFLEX;
      case '/':
	type = TOKEN_ITALIC_CORRECTION;
	return;
      case ',':
	type = TOKEN_NODE;
	nd = new left_italic_corrected_node;
	return;
      case '&':
	goto ESCAPE_AMPERSAND;
      case ')':
	goto ESCAPE_RIGHT_PARENTHESIS;
      case '!':
	goto ESCAPE_BANG;
      case '?':
	goto ESCAPE_QUESTION;
      case '~':
	goto ESCAPE_TILDE;
      case ':':
	goto ESCAPE_COLON;
      case '"':
	while ((cc = input_stack::get(0)) != '\n' && cc != EOF)
	  ;
	if (cc == '\n')
	  type = TOKEN_NEWLINE;
	else
	  type = TOKEN_EOF;
	return;
      case '#':			// Like \" but newline is ignored.
	while ((cc = input_stack::get(0)) != '\n')
	  if (cc == EOF) {
	    type = TOKEN_EOF;
	    return;
	  }
	break;
      case '$':
	{
	  symbol s = read_escape_parameter();
	  if (!(s.is_null() || s.is_empty()))
	    interpolate_arg(s);
	  break;
	}
      case '*':
	{
	  symbol s = read_escape_parameter(WITH_ARGS);
	  if (!(s.is_null() || s.is_empty())) {
	    if (have_multiple_params) {
	      have_multiple_params = false;
	      interpolate_string_with_args(s);
	    }
	    else
	      interpolate_string(s);
	  }
	  break;
	}
      case 'a':
	nd = new non_interpreted_char_node('\001');
	type = TOKEN_NODE;
	return;
      case 'A':
	c = '0' + do_name_test();
	type = TOKEN_CHAR;
	return;
      case 'b':
	nd = do_bracket();
	type = TOKEN_NODE;
	return;
      case 'B':
	c = '0' + do_expr_test();
	type = TOKEN_CHAR;
	return;
      case 'c':
	goto ESCAPE_c;
      case 'C':
	nm = get_delim_name();
	if (nm.is_null())
	  break;
	type = TOKEN_SPECIAL;
	return;
      case 'd':
	type = TOKEN_NODE;
	nd = new vmotion_node(curenv->get_size() / 2,
			      curenv->get_fill_color());
	return;
      case 'D':
	nd = read_draw_node();
	if (!nd)
	  break;
	type = TOKEN_NODE;
	return;
      case 'e':
	goto ESCAPE_e;
      case 'E':
	goto handle_escape_char;
      case 'f':
	{
	  symbol s = read_escape_parameter(ALLOW_EMPTY);
	  if (s.is_null())
	    break;
	  const char *p;
	  for (p = s.contents(); *p != '\0'; p++)
	    if (!csdigit(*p))
	      break;
	  // environment::set_font warns if a bogus mounting position is
	  // requested.  We must warn here if a bogus font name is
	  // selected.
	  if (*p != '\0' || s.is_empty()) {
	    if (!curenv->set_font(s))
	      warning(WARN_FONT, "cannot select font '%1'",
		      s.contents());
	  }
	  else
	    (void) curenv->set_font(atoi(s.contents()));
	  if (!compatible_flag)
	    have_input = 1;
	  break;
	}
      case 'F':
	{
	  symbol s = read_escape_parameter(ALLOW_EMPTY);
	  if (s.is_null())
	    break;
	  curenv->set_family(s);
	  have_input = 1;
	  break;
	}
      case 'g':
	{
	  symbol s = read_escape_parameter();
	  if (!(s.is_null() || s.is_empty()))
	    interpolate_number_format(s);
	  break;
	}
      case 'h':
	if (!get_delim_number(&x, 'm'))
	  break;
	type = TOKEN_HORIZONTAL_SPACE;
	nd = new hmotion_node(x, curenv->get_fill_color());
	return;
      case 'H':
	// don't take height increments relative to previous height if
	// in compatibility mode
	if (!compatible_flag && curenv->get_char_height()) {
	  if (get_delim_number(&x, 'z', curenv->get_char_height()))
	    curenv->set_char_height(x);
	}
	else {
	  if (get_delim_number(&x, 'z', curenv->get_requested_point_size()))
	    curenv->set_char_height(x);
	}
	if (!compatible_flag)
	  have_input = 1;
	break;
      case 'k':
	nm = read_escape_parameter();
	if (nm.is_null() || nm.is_empty())
	  break;
	type = TOKEN_MARK_INPUT;
	return;
      case 'l':
      case 'L':
	{
	  charinfo *s = 0;
	  if (!get_line_arg(&x, (cc == 'l' ? 'm': 'v'), &s))
	    break;
	  if (s == 0)
	    s = get_charinfo(cc == 'l' ? "ru" : "br");
	  type = TOKEN_NODE;
	  node *char_node = curenv->make_char_node(s);
	  if (cc == 'l')
	    nd = new hline_node(x, char_node);
	  else
	    nd = new vline_node(x, char_node);
	  return;
	}
      case 'm':
	do_glyph_color(read_escape_parameter(ALLOW_EMPTY));
	if (!compatible_flag)
	  have_input = 1;
	break;
      case 'M':
	do_fill_color(read_escape_parameter(ALLOW_EMPTY));
	if (!compatible_flag)
	  have_input = 1;
	break;
      case 'n':
	{
	  int inc;
	  symbol s = read_increment_and_escape_parameter(&inc);
	  if (!(s.is_null() || s.is_empty()))
	    interpolate_number_reg(s, inc);
	  break;
	}
      case 'N':
	if (!get_delim_number(&val, 0))
	  break;
	if (val < 0) {
	  warning(WARN_CHAR, "invalid numbered character %1", val);
	  break;
	}
	type = TOKEN_NUMBERED_CHAR;
	return;
      case 'o':
	nd = do_overstrike();
	type = TOKEN_NODE;
	return;
      case 'O':
	nd = do_suppress(read_escape_parameter());
	if (!nd)
	  break;
	type = TOKEN_NODE;
	return;
      case 'p':
	type = TOKEN_SPREAD;
	return;
      case 'r':
	type = TOKEN_NODE;
	nd = new vmotion_node(-curenv->get_size(), curenv->get_fill_color());
	return;
      case 'R':
	do_register();
	if (!compatible_flag)
	  have_input = 1;
	break;
      case 's':
	if (read_size(&x))
	  curenv->set_size(x);
	if (!compatible_flag)
	  have_input = 1;
	break;
      case 'S':
	if (get_delim_number(&x, 0))
	  curenv->set_char_slant(x);
	if (!compatible_flag)
	  have_input = 1;
	break;
      case 't':
	type = TOKEN_NODE;
	nd = new non_interpreted_char_node('\t');
	return;
      case 'u':
	type = TOKEN_NODE;
	nd = new vmotion_node(-curenv->get_size() / 2,
			      curenv->get_fill_color());
	return;
      case 'v':
	if (!get_delim_number(&x, 'v'))
	  break;
	type = TOKEN_NODE;
	nd = new vmotion_node(x, curenv->get_fill_color());
	return;
      case 'V':
	{
	  symbol s = read_escape_parameter();
	  if (!(s.is_null() || s.is_empty()))
	    interpolate_environment_variable(s);
	  break;
	}
      case 'w':
	do_width();
	break;
      case 'x':
	if (!get_delim_number(&x, 'v'))
	  break;
	type = TOKEN_NODE;
	nd = new extra_size_node(x);
	return;
      case 'X':
	nd = do_special();
	if (!nd)
	  break;
	type = TOKEN_NODE;
	return;
      case 'Y':
	{
	  symbol s = read_escape_parameter();
	  if (s.is_null() || s.is_empty())
	    break;
	  request_or_macro *p = lookup_request(s);
	  macro *m = p->to_macro();
	  if (!m) {
	    error("can't transparently throughput a request");
	    break;
	  }
	  nd = new special_node(*m);
	  type = TOKEN_NODE;
	  return;
	}
      case 'z':
	{
	  next();
	  if (type == TOKEN_NODE || type == TOKEN_HORIZONTAL_SPACE)
	    nd = new zero_width_node(nd);
	  else {
	    charinfo *ci = get_char(true /* required */);
	    if (ci == 0)
	      break;
	    node *gn = curenv->make_char_node(ci);
	    if (gn == 0)
	      break;
	    nd = new zero_width_node(gn);
	    type = TOKEN_NODE;
	  }
	  return;
	}
      case 'Z':
	nd = do_zero_width();
	if (nd == 0)
	  break;
	type = TOKEN_NODE;
	return;
      case '{':
	goto ESCAPE_LEFT_BRACE;
      case '}':
	goto ESCAPE_RIGHT_BRACE;
      case '\n':
	break;
      case '[':
	if (!compatible_flag) {
	  symbol s = read_long_escape_parameters(WITH_ARGS);
	  if (s.is_null() || s.is_empty())
	    break;
	  if (have_multiple_params) {
	    have_multiple_params = false;
	    nm = composite_glyph_name(s);
	  }
	  else {
	    const char *gn = check_unicode_name(s.contents());
	    if (gn) {
	      const char *gn_decomposed = decompose_unicode(gn);
	      if (gn_decomposed)
		gn = &gn_decomposed[1];
	      const char *groff_gn = unicode_to_glyph_name(gn);
	      if (groff_gn)
		nm = symbol(groff_gn);
	      else {
		char *buf = new char[strlen(gn) + 1 + 1];
		strcpy(buf, "u");
		strcat(buf, gn);
		nm = symbol(buf);
		delete[] buf;
	      }
	    }
	    else
	      nm = symbol(s.contents());
	  }
	  type = TOKEN_SPECIAL;
	  return;
	}
	goto handle_ordinary_char;
      default:
	if (cc != escape_char && cc != '.')
	  warning(WARN_ESCAPE, "escape character ignored before %1",
		  input_char_description(cc));
	goto handle_ordinary_char;
      }
    }
  }
}

int token::operator==(const token &t)
{
  if (type != t.type)
    return 0;
  switch(type) {
  case TOKEN_CHAR:
    return c == t.c;
  case TOKEN_SPECIAL:
    return nm == t.nm;
  case TOKEN_NUMBERED_CHAR:
    return val == t.val;
  default:
    return 1;
  }
}

int token::operator!=(const token &t)
{
  return !(*this == t);
}

// is token a suitable delimiter (like ')?

bool token::usable_as_delimiter(bool report_error)
{
  switch(type) {
  case TOKEN_CHAR:
    switch(c) {
    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
    case '+':
    case '-':
    case '/':
    case '*':
    case '%':
    case '<':
    case '>':
    case '=':
    case '&':
    case ':':
    case '(':
    case ')':
    case '.':
      if (report_error)
        error("character '%1' is not allowed as a starting delimiter",
	      char(c));
      return false;
    default:
      return true;
    }
  case TOKEN_NODE:
    // the user doesn't know what a node is
    if (report_error)
      error("missing argument or invalid starting delimiter");
    return false;
  case TOKEN_SPACE:
  case TOKEN_STRETCHABLE_SPACE:
  case TOKEN_UNSTRETCHABLE_SPACE:
  case TOKEN_HORIZONTAL_SPACE:
  case TOKEN_TAB:
  case TOKEN_NEWLINE:
    if (report_error)
      error("%1 is not allowed as a starting delimiter", description());
    return false;
  default:
    return true;
  }
}

const char *token::description()
{
  static char buf[4];
  switch (type) {
  case TOKEN_BACKSPACE:
    return "a backspace character";
  case TOKEN_CHAR:
    if (c == INPUT_DELETE)
      return "a delete character";
    else {
      buf[0] = '\'';
      buf[1] = c;
      buf[2] = '\'';
      buf[3] = '\0';
      return buf;
    }
  case TOKEN_DUMMY:
    return "an escaped '&'";
  case TOKEN_ESCAPE:
    return "an escaped 'e'";
  case TOKEN_HYPHEN_INDICATOR:
    return "an escaped '%'";
  case TOKEN_INTERRUPT:
    return "an escaped 'c'";
  case TOKEN_ITALIC_CORRECTION:
    return "an escaped '/'";
  case TOKEN_LEADER:
    return "a leader character";
  case TOKEN_LEFT_BRACE:
    return "an escaped '{'";
  case TOKEN_MARK_INPUT:
    return "an escaped 'k'";
  case TOKEN_NEWLINE:
    return "a newline";
  case TOKEN_NODE:
    return "a node";
  case TOKEN_NUMBERED_CHAR:
    return "an escaped 'N'";
  case TOKEN_RIGHT_BRACE:
    return "an escaped '}'";
  case TOKEN_SPACE:
    return "a space";
  case TOKEN_SPECIAL:
    return "a special character";
  case TOKEN_SPREAD:
    return "an escaped 'p'";
  case TOKEN_STRETCHABLE_SPACE:
    return "an escaped '~'";
  case TOKEN_UNSTRETCHABLE_SPACE:
    return "an escaped ' '";
  case TOKEN_HORIZONTAL_SPACE:
    return "a horizontal motion";
  case TOKEN_TAB:
    return "a tab character";
  case TOKEN_TRANSPARENT:
    return "an escaped '!'";
  case TOKEN_TRANSPARENT_DUMMY:
    return "an escaped ')'";
  case TOKEN_ZERO_WIDTH_BREAK:
    return "an escaped ':'";
  case TOKEN_EOF:
    return "end of input";
  default:
    break;
  }
  return "a magic token";
}

void skip_line()
{
  while (!tok.is_newline())
    if (tok.is_eof())
      return;
    else
      tok.next();
  tok.next();
}

void compatible()
{
  int n;
  if (has_arg() && get_integer(&n))
    compatible_flag = n != 0;
  else
    compatible_flag = 1;
  skip_line();
}

static void diagnose_missing_identifier(bool required)
{
  if (tok.is_newline() || tok.is_eof()) {
    if (required)
      warning(WARN_MISSING, "missing identifier");
  }
  else if (tok.is_right_brace() || tok.is_tab()) {
    const char *start = tok.description();
    do {
      tok.next();
    } while (tok.is_space() || tok.is_right_brace() || tok.is_tab());
    if (!tok.is_newline() && !tok.is_eof())
      error("%1 is not allowed before an argument", start);
    else if (required)
      warning(WARN_MISSING, "missing identifier");
  }
  else if (required)
    error("expected identifier, got %1", tok.description());
  else
    error("expected identifier, got %1; treated as missing",
	  tok.description());
}

static void diagnose_invalid_identifier()
{
  if (!tok.is_newline() && !tok.is_eof() && !tok.is_space()
      && !tok.is_tab() && !tok.is_right_brace()
      // We don't want to give a warning for .el\{
      && !tok.is_left_brace())
    error("%1 is not allowed in an identifier", tok.description());
}

symbol get_name(bool required)
{
  if (compatible_flag) {
    char buf[3];
    tok.skip();
    if ((buf[0] = tok.ch()) != 0) {
      tok.next();
      if ((buf[1] = tok.ch()) != 0) {
	buf[2] = 0;
	tok.make_space();
      }
      else
	diagnose_invalid_identifier();
      return symbol(buf);
    }
    else {
      diagnose_missing_identifier(required);
      return NULL_SYMBOL;
    }
  }
  else
    return get_long_name(required);
}

symbol get_long_name(bool required)
{
  return do_get_long_name(required, 0);
}

static symbol do_get_long_name(bool required, char end)
{
  while (tok.is_space())
    tok.next();
  char abuf[ABUF_SIZE];
  char *buf = abuf;
  int buf_size = ABUF_SIZE;
  int i = 0;
  for (;;) {
    // If end != 0 we normally have to append a null byte
    if (i + 2 > buf_size) {
      if (buf == abuf) {
	buf = new char[ABUF_SIZE*2];
	memcpy(buf, abuf, buf_size);
	buf_size = ABUF_SIZE*2;
      }
      else {
	char *old_buf = buf;
	buf = new char[buf_size*2];
	memcpy(buf, old_buf, buf_size);
	buf_size *= 2;
	delete[] old_buf;
      }
    }
    if ((buf[i] = tok.ch()) == 0 || buf[i] == end)
      break;
    i++;
    tok.next();
  }
  if (i == 0) {
    diagnose_missing_identifier(required);
    return NULL_SYMBOL;
  }
  if (end && buf[i] == end)
    buf[i+1] = '\0';
  else
    diagnose_invalid_identifier();
  if (buf == abuf)
    return symbol(buf);
  else {
    symbol s(buf);
    delete[] buf;
    return s;
  }
}

void exit_troff()
{
  is_exit_underway = true;
  topdiv->set_last_page();
  if (!end_of_input_macro_name.is_null()) {
    spring_trap(end_of_input_macro_name);
    tok.next();
    process_input_stack();
  }
  curenv->final_break();
  tok.next();
  process_input_stack();
  end_diversions();
  if (topdiv->get_page_length() > 0) {
    is_eoi_macro_finished = true;
    topdiv->set_ejecting();
    static unsigned char buf[2] = { LAST_PAGE_EJECTOR, '\0' };
    input_stack::push(make_temp_iterator((char *)buf));
    topdiv->space(topdiv->get_page_length(), 1);
    tok.next();
    process_input_stack();
    seen_last_page_ejector = 1;	// should be set already
    topdiv->set_ejecting();
    push_page_ejector();
    topdiv->space(topdiv->get_page_length(), 1);
    tok.next();
    process_input_stack();
  }
  cleanup_and_exit(EXIT_SUCCESS);
}

// This implements .ex.  The input stack must be cleared before calling
// exit_troff().

void exit_request()
{
  input_stack::clear();
  if (is_exit_underway)
    tok.next();
  else
    exit_troff();
}

void return_macro_request()
{
  if (has_arg() && tok.ch())
    input_stack::pop_macro();
  input_stack::pop_macro();
  tok.next();
}

void eoi_macro()
{
  end_of_input_macro_name = get_name();
  skip_line();
}

void blank_line_macro()
{
  blank_line_macro_name = get_name();
  skip_line();
}

void leading_spaces_macro()
{
  leading_spaces_macro_name = get_name();
  skip_line();
}

static void trapping_blank_line()
{
  if (!blank_line_macro_name.is_null())
    spring_trap(blank_line_macro_name);
  else
    blank_line();
}

void do_request()
{
  assert(do_old_compatible_flag == -1);
  do_old_compatible_flag = compatible_flag;
  compatible_flag = 0;
  symbol nm = get_name();
  if (nm.is_null())
    skip_line();
  else
    interpolate_macro(nm, true /* don't want next token */);
  compatible_flag = do_old_compatible_flag;
  do_old_compatible_flag = -1;
  request_or_macro *p = lookup_request(nm);
  macro *m = p->to_macro();
  if (m)
    tok.next();
}

inline int possibly_handle_first_page_transition()
{
  if (topdiv->before_first_page && curdiv == topdiv && !curenv->is_dummy()) {
    handle_first_page_transition();
    return 1;
  }
  else
    return 0;
}

static int transparent_translate(int cc)
{
  if (!is_invalid_input_char(cc)) {
    charinfo *ci = charset_table[cc];
    switch (ci->get_special_translation(1)) {
    case charinfo::TRANSLATE_SPACE:
      return ' ';
    case charinfo::TRANSLATE_STRETCHABLE_SPACE:
      return ESCAPE_TILDE;
    case charinfo::TRANSLATE_DUMMY:
      return ESCAPE_AMPERSAND;
    case charinfo::TRANSLATE_HYPHEN_INDICATOR:
      return ESCAPE_PERCENT;
    }
    // This is really ugly.
    ci = ci->get_translation(1);
    if (ci) {
      int c = ci->get_ascii_code();
      if (c != '\0')
	return c;
      if (getenv("GROFF_ENABLE_TRANSPARENCY_WARNINGS")
	  != 0 /* nullptr */)
	error("can't translate %1 to special character '%2'"
	      " in transparent throughput",
	      input_char_description(cc),
	      ci->nm.contents());
    }
  }
  return cc;
}

class int_stack {
  struct int_stack_element {
    int n;
    int_stack_element *next;
  } *top;
public:
  int_stack();
  ~int_stack();
  void push(int);
  int is_empty();
  int pop();
};

int_stack::int_stack()
{
  top = 0;
}

int_stack::~int_stack()
{
  while (top != 0) {
    int_stack_element *temp = top;
    top = top->next;
    delete temp;
  }
}

int int_stack::is_empty()
{
  return top == 0;
}

void int_stack::push(int n)
{
  int_stack_element *p = new int_stack_element;
  p->next = top;
  p->n = n;
  top = p;
}

int int_stack::pop()
{
  assert(top != 0);
  int_stack_element *p = top;
  top = top->next;
  int n = p->n;
  delete p;
  return n;
}

int node::reread(int *)
{
  return 0;
}

int global_diverted_space = 0;

int diverted_space_node::reread(int *bolp)
{
  global_diverted_space = 1;
  if (curenv->get_fill())
    trapping_blank_line();
  else
    curdiv->space(n);
  global_diverted_space = 0;
  *bolp = 1;
  return 1;
}

int diverted_copy_file_node::reread(int *bolp)
{
  curdiv->copy_file(filename.contents());
  *bolp = 1;
  return 1;
}

int word_space_node::reread(int *)
{
  if (unformat) {
    for (width_list *w = orig_width; w; w = w->next)
      curenv->space(w->width, w->sentence_width);
    unformat = 0;
    return 1;
  }
  return 0;
}

int unbreakable_space_node::reread(int *)
{
  return 0;
}

int hmotion_node::reread(int *)
{
  if (unformat && was_tab) {
    curenv->handle_tab(0);
    unformat = 0;
    return 1;
  }
  return 0;
}

static int leading_spaces_number = 0;
static int leading_spaces_space = 0;

void process_input_stack()
{
  int_stack trap_bol_stack;
  int bol = 1;
  for (;;) {
    int suppress_next = 0;
    switch (tok.type) {
    case token::TOKEN_CHAR:
      {
	unsigned char ch = tok.c;
	if (bol && !have_input
	    && (ch == curenv->control_char
		|| ch == curenv->no_break_control_char)) {
	  break_flag = ch == curenv->control_char;
	  // skip tabs as well as spaces here
	  do {
	    tok.next();
	  } while (tok.is_white_space());
	  symbol nm = get_name();
#if defined(DEBUGGING)
	  if (debug_state) {
	    if (! nm.is_null()) {
	      if (strcmp(nm.contents(), "test") == 0) {
		fprintf(stderr, "found it!\n");
		fflush(stderr);
	      }
	      fprintf(stderr, "interpreting [%s]", nm.contents());
	      if (strcmp(nm.contents(), "di") == 0 && topdiv != curdiv)
		fprintf(stderr, " currently in diversion: %s",
			curdiv->get_diversion_name());
	      fprintf(stderr, "\n");
	      fflush(stderr);
	    }
	  }
#endif
	  if (nm.is_null())
	    skip_line();
	  else {
	    interpolate_macro(nm);
#if defined(DEBUGGING)
	    if (debug_state) {
	      fprintf(stderr, "finished interpreting [%s] and environment state is\n", nm.contents());
	      curenv->dump_troff_state();
	    }
#endif
	  }
	  suppress_next = 1;
	}
	else {
	  if (possibly_handle_first_page_transition())
	    ;
	  else {
	    for (;;) {
#if defined(DEBUGGING)
	      if (debug_state) {
		fprintf(stderr, "found [%c]\n", ch); fflush(stderr);
	      }
#endif
	      curenv->add_char(charset_table[ch]);
	      tok.next();
	      if (tok.type != token::TOKEN_CHAR)
		break;
	      ch = tok.c;
	    }
	    suppress_next = 1;
	    bol = 0;
	  }
	}
	break;
      }
    case token::TOKEN_TRANSPARENT:
      {
	if (bol) {
	  if (possibly_handle_first_page_transition())
	    ;
	  else {
	    int cc;
	    do {
	      node *n;
	      cc = get_copy(&n);
	      if (cc != EOF) {
		if (cc != '\0')
		  curdiv->transparent_output(transparent_translate(cc));
		else
		  curdiv->transparent_output(n);
	      }
	    } while (cc != '\n' && cc != EOF);
	    if (cc == EOF)
	      curdiv->transparent_output('\n');
	  }
	}
	break;
      }
    case token::TOKEN_NEWLINE:
      {
	if (bol && !old_have_input
	    && !curenv->get_prev_line_interrupted())
	  trapping_blank_line();
	else {
	  curenv->newline();
	  bol = 1;
	}
	break;
      }
    case token::TOKEN_REQUEST:
      {
	int request_code = tok.c;
	tok.next();
	switch (request_code) {
	case TITLE_REQUEST:
	  title();
	  break;
	case COPY_FILE_REQUEST:
	  copy_file();
	  break;
	case TRANSPARENT_FILE_REQUEST:
	  transparent_file();
	  break;
#ifdef COLUMN
	case VJUSTIFY_REQUEST:
	  vjustify();
	  break;
#endif /* COLUMN */
	default:
	  assert(0);
	  break;
	}
	suppress_next = 1;
	break;
      }
    case token::TOKEN_SPACE:
      {
	if (possibly_handle_first_page_transition())
	  ;
	else if (bol && !curenv->get_prev_line_interrupted()) {
	  int nspaces = 0;
	  // save space_width now so that it isn't changed by \f or \s
	  // which we wouldn't notice here
	  hunits space_width = curenv->get_space_width();
	  do {
	    nspaces += tok.nspaces();
	    tok.next();
	  } while (tok.is_space());
	  if (tok.is_newline())
	    trapping_blank_line();
	  else {
	    push_token(tok);
	    leading_spaces_number = nspaces;
	    leading_spaces_space = space_width.to_units() * nspaces;
	    if (!leading_spaces_macro_name.is_null())
	      spring_trap(leading_spaces_macro_name);
	    else {
	      curenv->do_break();
	      curenv->add_node(new hmotion_node(space_width * nspaces,
						curenv->get_fill_color()));
	    }
	    bol = 0;
	  }
	}
	else {
	  curenv->space();
	  bol = 0;
	}
	break;
      }
    case token::TOKEN_EOF:
      return;
    case token::TOKEN_NODE:
    case token::TOKEN_HORIZONTAL_SPACE:
      {
	if (possibly_handle_first_page_transition())
	  ;
	else if (tok.nd->reread(&bol)) {
	  delete tok.nd;
	  tok.nd = 0;
	}
	else {
	  curenv->add_node(tok.nd);
	  tok.nd = 0;
	  bol = 0;
	  curenv->possibly_break_line(1);
	}
	break;
      }
    case token::TOKEN_PAGE_EJECTOR:
      {
	continue_page_eject();
	// I think we just want to preserve bol.
	// bol = 1;
	break;
      }
    case token::TOKEN_BEGIN_TRAP:
      {
	trap_bol_stack.push(bol);
	bol = 1;
	have_input = 0;
	break;
      }
    case token::TOKEN_END_TRAP:
      {
	if (trap_bol_stack.is_empty())
	  error("spurious end trap token detected!");
	else
	  bol = trap_bol_stack.pop();
	have_input = 0;

	/* I'm not totally happy about this.  But I can't think of any other
	  way to do it.  Doing an output_pending_lines() whenever a
	  TOKEN_END_TRAP is detected doesn't work: for example,

	  .wh -1i x
	  .de x
	  'bp
	  ..
	  .wh -.5i y
	  .de y
	  .tl ''-%-''
	  ..
	  .br
	  .ll .5i
	  .sp |\n(.pu-1i-.5v
	  a\%very\%very\%long\%word

	  will print all but the first lines from the word immediately
	  after the footer, rather than on the next page. */

	if (trap_bol_stack.is_empty())
	  curenv->output_pending_lines();
	break;
      }
    default:
      {
	bol = 0;
	tok.process();
	break;
      }
    }
    if (!suppress_next)
      tok.next();
    trap_sprung_flag = 0;
  }
}

#ifdef WIDOW_CONTROL

void flush_pending_lines()
{
  while (!tok.is_newline() && !tok.is_eof())
    tok.next();
  curenv->output_pending_lines();
  tok.next();
}

#endif /* WIDOW_CONTROL */

request_or_macro::request_or_macro()
{
}

macro *request_or_macro::to_macro()
{
  return 0;
}

request::request(REQUEST_FUNCP pp) : p(pp)
{
}

void request::invoke(symbol, bool)
{
  (*p)();
}

struct char_block {
  enum { SIZE = 128 };
  unsigned char s[SIZE];
  char_block *next;
  char_block();
};

char_block::char_block()
: next(0)
{
}

class char_list {
public:
  char_list();
  ~char_list();
  void append(unsigned char);
  void set(unsigned char, int);
  unsigned char get(int);
  int length();
private:
  unsigned char *ptr;
  int len;
  char_block *head;
  char_block *tail;
  friend class macro_header;
  friend class string_iterator;
};

char_list::char_list()
: ptr(0), len(0), head(0), tail(0)
{
}

char_list::~char_list()
{
  while (head != 0) {
    char_block *tem = head;
    head = head->next;
    delete tem;
  }
}

int char_list::length()
{
  return len;
}

void char_list::append(unsigned char c)
{
  if (tail == 0) {
    head = tail = new char_block;
    ptr = tail->s;
  }
  else {
    if (ptr >= tail->s + char_block::SIZE) {
      tail->next = new char_block;
      tail = tail->next;
      ptr = tail->s;
    }
  }
  *ptr++ = c;
  len++;
}

void char_list::set(unsigned char c, int offset)
{
  assert(len > offset);
  // optimization for access at the end
  int boundary = len - len % char_block::SIZE;
  if (offset >= boundary) {
    *(tail->s + offset - boundary) = c;
    return;
  }
  char_block *tem = head;
  int l = 0;
  for (;;) {
    l += char_block::SIZE;
    if (l > offset) {
      *(tem->s + offset % char_block::SIZE) = c;
      return;
    }
    tem = tem->next;
  }
}

unsigned char char_list::get(int offset)
{
  assert(len > offset);
  // optimization for access at the end
  int boundary = len - len % char_block::SIZE;
  if (offset >= boundary)
    return *(tail->s + offset - boundary);
  char_block *tem = head;
  int l = 0;
  for (;;) {
    l += char_block::SIZE;
    if (l > offset)
      return *(tem->s + offset % char_block::SIZE);
    tem = tem->next;
  }
}

class node_list {
  node *head;
  node *tail;
public:
  node_list();
  ~node_list();
  void append(node *);
  int length();
  node *extract();

  friend class macro_header;
  friend class string_iterator;
};

void node_list::append(node *n)
{
  if (head == 0) {
    n->next = 0;
    head = tail = n;
  }
  else {
    n->next = 0;
    tail = tail->next = n;
  }
}

int node_list::length()
{
  int total = 0;
  for (node *n = head; n != 0; n = n->next)
    ++total;
  return total;
}

node_list::node_list()
{
  head = tail = 0;
}

node *node_list::extract()
{
  node *temp = head;
  head = tail = 0;
  return temp;
}

node_list::~node_list()
{
  delete_node_list(head);
}

class macro_header {
public:
  int count;
  char_list cl;
  node_list nl;
  macro_header() { count = 1; }
  macro_header *copy(int);
};

macro::~macro()
{
  if (p != 0 && --(p->count) <= 0)
    delete p;
}

macro::macro()
: is_a_diversion(0), is_a_string(1)
{
  if (!input_stack::get_location(1, &filename, &lineno)) {
    filename = 0;
    lineno = 0;
  }
  len = 0;
  empty_macro = 1;
  p = 0;
}

macro::macro(const macro &m)
: filename(m.filename), lineno(m.lineno), len(m.len),
  empty_macro(m.empty_macro), is_a_diversion(m.is_a_diversion),
  is_a_string(m.is_a_string), p(m.p)
{
  if (p != 0)
    p->count++;
}

macro::macro(int is_div)
: is_a_diversion(is_div)
{
  if (!input_stack::get_location(1, &filename, &lineno)) {
    filename = 0;
    lineno = 0;
  }
  len = 0;
  empty_macro = 1;
  is_a_string = 1;
  p = 0;
}

int macro::is_diversion()
{
  return is_a_diversion;
}

int macro::is_string()
{
  return is_a_string;
}

void macro::clear_string_flag()
{
  is_a_string = 0;
}

macro &macro::operator=(const macro &m)
{
  // don't assign object
  if (m.p != 0)
    m.p->count++;
  if (p != 0 && --(p->count) <= 0)
    delete p;
  p = m.p;
  filename = m.filename;
  lineno = m.lineno;
  len = m.len;
  empty_macro = m.empty_macro;
  is_a_diversion = m.is_a_diversion;
  is_a_string = m.is_a_string;
  return *this;
}

void macro::append(unsigned char c)
{
  assert(c != 0);
  if (p == 0)
    p = new macro_header;
  if (p->cl.length() != len) {
    macro_header *tem = p->copy(len);
    if (--(p->count) <= 0)
      delete p;
    p = tem;
  }
  p->cl.append(c);
  ++len;
  if (c != PUSH_GROFF_MODE && c != PUSH_COMP_MODE && c != POP_GROFFCOMP_MODE)
    empty_macro = 0;
}

void macro::set(unsigned char c, int offset)
{
  assert(p != 0);
  assert(c != 0);
  p->cl.set(c, offset);
}

unsigned char macro::get(int offset)
{
  assert(p != 0);
  return p->cl.get(offset);
}

int macro::length()
{
  return len;
}

void macro::append_str(const char *s)
{
  int i = 0;

  if (s) {
    while (s[i] != (char)0) {
      append(s[i]);
      i++;
    }
  }
}

void macro::append(node *n)
{
  assert(n != 0);
  if (p == 0)
    p = new macro_header;
  if (p->cl.length() != len) {
    macro_header *tem = p->copy(len);
    if (--(p->count) <= 0)
      delete p;
    p = tem;
  }
  p->cl.append(0);
  p->nl.append(n);
  ++len;
  empty_macro = 0;
}

void macro::append_unsigned(unsigned int i)
{
  unsigned int j = i / 10;
  if (j != 0)
    append_unsigned(j);
  append(((unsigned char)(((int)'0') + i % 10)));
}

void macro::append_int(int i)
{
  if (i < 0) {
    append('-');
    i = -i;
  }
  append_unsigned((unsigned int)i);
}

void macro::print_size()
{
  errprint("%1", len);
}

// make a copy of the first n bytes

macro_header *macro_header::copy(int n)
{
  macro_header *p = new macro_header;
  char_block *bp = cl.head;
  unsigned char *ptr = bp->s;
  node *nd = nl.head;
  while (--n >= 0) {
    if (ptr >= bp->s + char_block::SIZE) {
      bp = bp->next;
      ptr = bp->s;
    }
    unsigned char c = *ptr++;
    p->cl.append(c);
    if (c == 0) {
      p->nl.append(nd->copy());
      nd = nd->next;
    }
  }
  return p;
}

void print_macros()
{
  object_dictionary_iterator iter(request_dictionary);
  request_or_macro *rm;
  symbol s;
  while (iter.get(&s, (object **)&rm)) {
    assert(!s.is_null());
    macro *m = rm->to_macro();
    if (m) {
      errprint("%1\t", s.contents());
      m->print_size();
      errprint("\n");
    }
  }
  fflush(stderr);
  skip_line();
}

class string_iterator : public input_iterator {
  macro mac;
  const char *how_invoked;
  int newline_flag;
  int lineno;
  char_block *bp;
  int count;			// of characters remaining
  node *nd;
  int saved_compatible_flag;
  int with_break;		// inherited from the caller
protected:
  symbol nm;
  string_iterator();
public:
  string_iterator(const macro &, const char * = 0, symbol = NULL_SYMBOL);
  int fill(node **);
  int peek();
  int get_location(int, const char **, int *);
  void backtrace();
  int get_break_flag() { return with_break; }
  void save_compatible_flag(int f) { saved_compatible_flag = f; }
  int get_compatible_flag() { return saved_compatible_flag; }
  int is_diversion();
};

string_iterator::string_iterator(const macro &m, const char *p, symbol s)
: input_iterator(m.is_a_diversion), mac(m), how_invoked(p), newline_flag(0),
  lineno(1), nm(s)
{
  count = mac.len;
  if (count != 0) {
    bp = mac.p->cl.head;
    nd = mac.p->nl.head;
    ptr = eptr = bp->s;
  }
  else {
    bp = 0;
    nd = 0;
    ptr = eptr = 0;
  }
  with_break = input_stack::get_break_flag();
}

string_iterator::string_iterator()
{
  bp = 0;
  nd = 0;
  ptr = eptr = 0;
  newline_flag = 0;
  how_invoked = 0;
  lineno = 1;
  count = 0;
  with_break = input_stack::get_break_flag();
}

int string_iterator::is_diversion()
{
  return mac.is_diversion();
}

int string_iterator::fill(node **np)
{
  if (newline_flag)
    lineno++;
  newline_flag = 0;
  if (count <= 0)
    return EOF;
  const unsigned char *p = eptr;
  if (p >= bp->s + char_block::SIZE) {
    bp = bp->next;
    p = bp->s;
  }
  if (*p == '\0') {
    if (np) {
      *np = nd->copy();
      if (is_diversion())
	(*np)->div_nest_level = input_stack::get_div_level();
      else
	(*np)->div_nest_level = 0;
    }
    nd = nd->next;
    eptr = ptr = p + 1;
    count--;
    return 0;
  }
  const unsigned char *e = bp->s + char_block::SIZE;
  if (e - p > count)
    e = p + count;
  ptr = p;
  while (p < e) {
    unsigned char c = *p;
    if (c == '\n' || c == ESCAPE_NEWLINE) {
      newline_flag = 1;
      p++;
      break;
    }
    if (c == '\0')
      break;
    p++;
  }
  eptr = p;
  count -= p - ptr;
  return *ptr++;
}

int string_iterator::peek()
{
  if (count <= 0)
    return EOF;
  const unsigned char *p = eptr;
  if (p >= bp->s + char_block::SIZE) {
    p = bp->next->s;
  }
  return *p;
}

int string_iterator::get_location(int allow_macro,
				  const char **filep, int *linep)
{
  if (!allow_macro)
    return 0;
  if (mac.filename == 0)
    return 0;
  *filep = mac.filename;
  *linep = mac.lineno + lineno - 1;
  return 1;
}

void string_iterator::backtrace()
{
  if (mac.filename) {
    if (program_name)
      fprintf(stderr, "%s: ", program_name);
    errprint("backtrace: '%1':%2", mac.filename,
	     mac.lineno + lineno - 1);
    if (how_invoked) {
      if (!nm.is_null())
	errprint(": %1 '%2'\n", how_invoked, nm.contents());
      else
	errprint(": %1\n", how_invoked);
    }
    else
      errprint("\n");
  }
}

class temp_iterator : public input_iterator {
  unsigned char *base;
  temp_iterator(const char *, int len);
public:
  ~temp_iterator();
  friend input_iterator *make_temp_iterator(const char *);
};

#ifdef __GNUG__
inline
#endif
temp_iterator::temp_iterator(const char *s, int len)
{
  base = new unsigned char[len];
  if (len > 0)
    memcpy(base, s, len);
  ptr = base;
  eptr = base + len;
}

temp_iterator::~temp_iterator()
{
  delete[] base;
}


input_iterator *make_temp_iterator(const char *s)
{
  if (s == 0)
    return new temp_iterator(s, 0);
  else {
    int n = strlen(s);
    return new temp_iterator(s, n);
  }
}

// this is used when macros with arguments are interpolated

struct arg_list {
  macro mac;
  int space_follows;
  arg_list *next;
  arg_list(const macro &, int);
  arg_list(const arg_list *);
  ~arg_list();
};

arg_list::arg_list(const macro &m, int s) : mac(m), space_follows(s), next(0)
{
}

arg_list::arg_list(const arg_list *al)
: next(0)
{
  mac = al->mac;
  space_follows = al->space_follows;
  arg_list **a = &next;
  arg_list *p = al->next;
  while (p) {
    *a = new arg_list(p->mac, p->space_follows);
    p = p->next;
    a = &(*a)->next;
  }
}

arg_list::~arg_list()
{
}

class macro_iterator : public string_iterator {
  arg_list *args;
  int argc;
  int with_break;		// whether called as .foo or 'foo
public:
  macro_iterator(symbol, macro &, const char * = "macro", int = 0);
  macro_iterator();
  ~macro_iterator();
  int has_args() { return 1; }
  input_iterator *get_arg(int);
  arg_list *get_arg_list();
  symbol get_macro_name();
  int space_follows_arg(int);
  int get_break_flag() { return with_break; }
  int nargs() { return argc; }
  void add_arg(const macro &, int);
  void shift(int);
  int is_macro() { return 1; }
  int is_diversion();
};

input_iterator *macro_iterator::get_arg(int i)
{
  if (i == 0)
    return make_temp_iterator(nm.contents());
  if (i > 0 && i <= argc) {
    arg_list *p = args;
    for (int j = 1; j < i; j++) {
      assert(p != 0);
      p = p->next;
    }
    return new string_iterator(p->mac);
  }
  else
    return 0;
}

arg_list *macro_iterator::get_arg_list()
{
  return args;
}

symbol macro_iterator::get_macro_name()
{
  return nm;
}

int macro_iterator::space_follows_arg(int i)
{
  if (i > 0 && i <= argc) {
    arg_list *p = args;
    for (int j = 1; j < i; j++) {
      assert(p != 0);
      p = p->next;
    }
    return p->space_follows;
  }
  else
    return 0;
}

void macro_iterator::add_arg(const macro &m, int s)
{
  arg_list **p;
  for (p = &args; *p; p = &((*p)->next))
    ;
  *p = new arg_list(m, s);
  ++argc;
}

void macro_iterator::shift(int n)
{
  while (n > 0 && argc > 0) {
    arg_list *tem = args;
    args = args->next;
    delete tem;
    --argc;
    --n;
  }
}

// This gets used by, e.g., .if '\?xxx\?''.

int operator==(const macro &m1, const macro &m2)
{
  if (m1.len != m2.len)
    return 0;
  string_iterator iter1(m1);
  string_iterator iter2(m2);
  int n = m1.len;
  while (--n >= 0) {
    node *nd1 = 0;
    int c1 = iter1.get(&nd1);
    assert(c1 != EOF);
    node *nd2 = 0;
    int c2 = iter2.get(&nd2);
    assert(c2 != EOF);
    if (c1 != c2) {
      if (c1 == 0)
	delete nd1;
      else if (c2 == 0)
	delete nd2;
      return 0;
    }
    if (c1 == 0) {
      assert(nd1 != 0);
      assert(nd2 != 0);
      int are_same = nd1->type() == nd2->type() && nd1->same(nd2);
      delete nd1;
      delete nd2;
      if (!are_same)
	return 0;
    }
  }
  return 1;
}

static void interpolate_macro(symbol nm, bool do_not_want_next_token)
{
  request_or_macro *p = (request_or_macro *)request_dictionary.lookup(nm);
  if (p == 0) {
    int warned = 0;
    const char *s = nm.contents();
    if (strlen(s) > 2) {
      request_or_macro *r;
      char buf[3];
      buf[0] = s[0];
      buf[1] = s[1];
      buf[2] = '\0';
      r = (request_or_macro *)request_dictionary.lookup(symbol(buf));
      if (r) {
	macro *m = r->to_macro();
	if (!m || !m->empty())
	  warned = warning(WARN_SPACE,
			   "macro '%1' not defined "
			   "(possibly missing space after '%2')",
			   nm.contents(), buf);
      }
    }
    if (!warned) {
      warning(WARN_MAC, "macro '%1' not defined", nm.contents());
      p = new macro;
      request_dictionary.define(nm, p);
    }
  }
  if (p)
    p->invoke(nm, do_not_want_next_token);
  else {
    skip_line();
    return;
  }
}

static void decode_args(macro_iterator *mi)
{
  if (!tok.is_newline() && !tok.is_eof()) {
    node *n;
    int c = get_copy(&n);
    for (;;) {
      while (c == ' ')
	c = get_copy(&n);
      if (c == '\n' || c == EOF)
	break;
      macro arg;
      int quote_input_level = 0;
      int done_tab_warning = 0;
      arg.append(compatible_flag ? PUSH_COMP_MODE : PUSH_GROFF_MODE);
      // we store discarded double quotes for \$^
      if (c == '"') {
	arg.append(DOUBLE_QUOTE);
	quote_input_level = input_stack::get_level();
	c = get_copy(&n);
      }
      while (c != EOF && c != '\n' && !(c == ' ' && quote_input_level == 0)) {
	if (quote_input_level > 0 && c == '"'
	    && (compatible_flag
		|| input_stack::get_level() == quote_input_level)) {
	  arg.append(DOUBLE_QUOTE);
	  c = get_copy(&n);
	  if (c == '"') {
	    arg.append(c);
	    c = get_copy(&n);
	  }
	  else
	    break;
	}
	else {
	  if (c == 0)
	    arg.append(n);
	  else {
	    if (c == '\t' && quote_input_level == 0 && !done_tab_warning) {
	      warning(WARN_TAB, "tab character in unquoted macro argument");
	      done_tab_warning = 1;
	    }
	    arg.append(c);
	  }
	  c = get_copy(&n);
	}
      }
      arg.append(POP_GROFFCOMP_MODE);
      mi->add_arg(arg, (c == ' '));
    }
  }
}

static void decode_string_args(macro_iterator *mi)
{
  node *n;
  int c = get_copy(&n);
  for (;;) {
    while (c == ' ')
      c = get_copy(&n);
    if (c == '\n' || c == EOF) {
      error("missing ']'");
      break;
    }
    if (c == ']')
      break;
    macro arg;
    int quote_input_level = 0;
    int done_tab_warning = 0;
    if (c == '"') {
      quote_input_level = input_stack::get_level();
      c = get_copy(&n);
    }
    while (c != EOF && c != '\n'
	   && !(c == ']' && quote_input_level == 0)
	   && !(c == ' ' && quote_input_level == 0)) {
      if (quote_input_level > 0 && c == '"'
	  && input_stack::get_level() == quote_input_level) {
	c = get_copy(&n);
	if (c == '"') {
	  arg.append(c);
	  c = get_copy(&n);
	}
	else
	  break;
      }
      else {
	if (c == 0)
	  arg.append(n);
	else {
	  if (c == '\t' && quote_input_level == 0 && !done_tab_warning) {
	    warning(WARN_TAB, "tab character in unquoted string argument");
	    done_tab_warning = 1;
	  }
	  arg.append(c);
	}
	c = get_copy(&n);
      }
    }
    mi->add_arg(arg, (c == ' '));
  }
}

void macro::invoke(symbol nm, bool do_not_want_next_token)
{
  macro_iterator *mi = new macro_iterator(nm, *this);
  decode_args(mi);
  input_stack::push(mi);
  // we must delay tok.next() in case the function has been called by
  // do_request to assure proper handling of compatible_flag
  if (!do_not_want_next_token)
    tok.next();
}

macro *macro::to_macro()
{
  return this;
}

int macro::empty()
{
  return empty_macro == 1;
}

macro_iterator::macro_iterator(symbol s, macro &m, const char *how_called,
			       int init_args)
: string_iterator(m, how_called, s), args(0), argc(0), with_break(break_flag)
{
  if (init_args) {
    arg_list *al = input_stack::get_arg_list();
    if (al) {
      args = new arg_list(al);
      argc = input_stack::nargs();
    }
  }
}

macro_iterator::macro_iterator() : args(0), argc(0), with_break(break_flag)
{
}

macro_iterator::~macro_iterator()
{
  while (args != 0) {
    arg_list *tem = args;
    args = args->next;
    delete tem;
  }
}

dictionary composite_dictionary(17);

void composite_request()
{
  symbol from = get_name(true /* required */);
  if (!from.is_null()) {
    const char *from_gn = glyph_name_to_unicode(from.contents());
    if (!from_gn) {
      from_gn = check_unicode_name(from.contents());
      if (!from_gn) {
	error("invalid composite glyph name '%1'", from.contents());
	skip_line();
	return;
      }
    }
    const char *from_decomposed = decompose_unicode(from_gn);
    if (from_decomposed)
      from_gn = &from_decomposed[1];
    symbol to = get_name(true /* required */);
    if (to.is_null())
      composite_dictionary.remove(symbol(from_gn));
    else {
      const char *to_gn = glyph_name_to_unicode(to.contents());
      if (!to_gn) {
	to_gn = check_unicode_name(to.contents());
	if (!to_gn) {
	  error("invalid composite glyph name '%1'", to.contents());
	  skip_line();
	  return;
	}
      }
      const char *to_decomposed = decompose_unicode(to_gn);
      if (to_decomposed)
	to_gn = &to_decomposed[1];
      if (strcmp(from_gn, to_gn) == 0)
	composite_dictionary.remove(symbol(from_gn));
      else
	(void)composite_dictionary.lookup(symbol(from_gn), (void *)to_gn);
    }
  }
  skip_line();
}

static symbol composite_glyph_name(symbol nm)
{
  macro_iterator *mi = new macro_iterator();
  decode_string_args(mi);
  input_stack::push(mi);
  const char *gn = glyph_name_to_unicode(nm.contents());
  if (!gn) {
    gn = check_unicode_name(nm.contents());
    if (!gn) {
      error("invalid base glyph '%1' in composite glyph name", nm.contents());
      return EMPTY_SYMBOL;
    }
  }
  const char *gn_decomposed = decompose_unicode(gn);
  string glyph_name(gn_decomposed ? &gn_decomposed[1] : gn);
  string gl;
  int n = input_stack::nargs();
  for (int i = 1; i <= n; i++) {
    glyph_name += '_';
    input_iterator *p = input_stack::get_arg(i);
    gl.clear();
    int c;
    while ((c = p->get(0)) != EOF)
      if (c != DOUBLE_QUOTE)
	gl += c;
    gl += '\0';
    const char *u = glyph_name_to_unicode(gl.contents());
    if (!u) {
      u = check_unicode_name(gl.contents());
      if (!u) {
	error("invalid component '%1' in composite glyph name",
	      gl.contents());
	return EMPTY_SYMBOL;
      }
    }
    const char *decomposed = decompose_unicode(u);
    if (decomposed)
      u = &decomposed[1];
    void *mapped_composite = composite_dictionary.lookup(symbol(u));
    if (mapped_composite)
      u = (const char *)mapped_composite;
    glyph_name += u;
  }
  glyph_name += '\0';
  const char *groff_gn = unicode_to_glyph_name(glyph_name.contents());
  if (groff_gn)
    return symbol(groff_gn);
  gl.clear();
  gl += 'u';
  gl += glyph_name;
  return symbol(gl.contents());
}

int trap_sprung_flag = 0;
int postpone_traps_flag = 0;
symbol postponed_trap;

void spring_trap(symbol nm)
{
  assert(!nm.is_null());
  trap_sprung_flag = 1;
  if (postpone_traps_flag) {
    postponed_trap = nm;
    return;
  }
  static char buf[2] = { BEGIN_TRAP, '\0' };
  static char buf2[2] = { END_TRAP, '\0' };
  input_stack::push(make_temp_iterator(buf2));
  request_or_macro *p = lookup_request(nm);
  // We don't perform this validation at the time the trap is planted
  // because a request name might be replaced by a macro by the time the
  // trap springs.
  macro *m = p->to_macro();
  if (m)
    input_stack::push(new macro_iterator(nm, *m, "trap-called macro"));
  else
    error("trap failed to spring: '%1' is a request", nm.contents());
  input_stack::push(make_temp_iterator(buf));
}

void postpone_traps()
{
  postpone_traps_flag = 1;
}

int unpostpone_traps()
{
  postpone_traps_flag = 0;
  if (!postponed_trap.is_null()) {
    spring_trap(postponed_trap);
    postponed_trap = NULL_SYMBOL;
    return 1;
  }
  else
    return 0;
}

void read_request()
{
  macro_iterator *mi = new macro_iterator;
  int reading_from_terminal = isatty(fileno(stdin));
  int had_prompt = 0;
  if (!tok.is_newline() && !tok.is_eof()) {
    int c = get_copy(0);
    while (c == ' ')
      c = get_copy(0);
    while (c != EOF && c != '\n' && c != ' ') {
      if (!is_invalid_input_char(c)) {
	if (reading_from_terminal)
	  fputc(c, stderr);
	had_prompt = 1;
      }
      c = get_copy(0);
    }
    if (c == ' ') {
      tok.make_space();
      decode_args(mi);
    }
  }
  if (reading_from_terminal) {
    fputc(had_prompt ? ':' : '\a', stderr);
    fflush(stderr);
  }
  input_stack::push(mi);
  macro mac;
  int nl = 0;
  int c;
  while ((c = getchar()) != EOF) {
    if (is_invalid_input_char(c))
      warning(WARN_INPUT, "invalid input character code %1", int(c));
    else {
      if (c == '\n') {
	if (nl)
	  break;
	else
	  nl = 1;
      }
      else
	nl = 0;
      mac.append(c);
    }
  }
  if (reading_from_terminal)
    clearerr(stdin);
  input_stack::push(new string_iterator(mac));
  tok.next();
}

enum define_mode { DEFINE_NORMAL, DEFINE_APPEND, DEFINE_IGNORE };
enum calling_mode { CALLING_NORMAL, CALLING_INDIRECT };
enum comp_mode { COMP_IGNORE, COMP_DISABLE, COMP_ENABLE };

void do_define_string(define_mode mode, comp_mode comp)
{
  symbol nm;
  node *n = 0;		// pacify compiler
  int c;
  nm = get_name(true /* required */);
  if (nm.is_null()) {
    skip_line();
    return;
  }
  if (tok.is_newline())
    c = '\n';
  else if (tok.is_tab())
    c = '\t';
  else if (!tok.is_space()) {
    error("bad string definition");
    skip_line();
    return;
  }
  else
    c = get_copy(&n);
  while (c == ' ')
    c = get_copy(&n);
  if (c == '"')
    c = get_copy(&n);
  macro mac;
  request_or_macro *rm = (request_or_macro *)request_dictionary.lookup(nm);
  macro *mm = rm ? rm->to_macro() : 0;
  if (mode == DEFINE_APPEND && mm)
    mac = *mm;
  if (comp == COMP_DISABLE)
    mac.append(PUSH_GROFF_MODE);
  else if (comp == COMP_ENABLE)
    mac.append(PUSH_COMP_MODE);
  while (c != '\n' && c != EOF) {
    if (c == 0)
      mac.append(n);
    else
      mac.append((unsigned char)c);
    c = get_copy(&n);
  }
  if (comp == COMP_DISABLE || comp == COMP_ENABLE)
    mac.append(POP_GROFFCOMP_MODE);
  if (!mm) {
    mm = new macro;
    request_dictionary.define(nm, mm);
  }
  *mm = mac;
  tok.next();
}

void define_string()
{
  do_define_string(DEFINE_NORMAL,
		   compatible_flag ? COMP_ENABLE: COMP_IGNORE);
}

void define_nocomp_string()
{
  do_define_string(DEFINE_NORMAL, COMP_DISABLE);
}

void append_string()
{
  do_define_string(DEFINE_APPEND,
		   compatible_flag ? COMP_ENABLE : COMP_IGNORE);
}

void append_nocomp_string()
{
  do_define_string(DEFINE_APPEND, COMP_DISABLE);
}

void do_define_character(char_mode mode, const char *font_name)
{
  node *n = 0;		// pacify compiler
  int c;
  tok.skip();
  charinfo *ci = tok.get_char(true /* required */);
  if (ci == 0) {
    skip_line();
    return;
  }
  if (font_name) {
    string s(font_name);
    s += ' ';
    s += ci->nm.contents();
    s += '\0';
    ci = get_charinfo(symbol(s.contents()));
  }
  tok.next();
  if (tok.is_newline())
    c = '\n';
  else if (tok.is_tab())
    c = '\t';
  else if (!tok.is_space()) {
    error("bad character definition");
    skip_line();
    return;
  }
  else
    c = get_copy(&n);
  while (c == ' ' || c == '\t')
    c = get_copy(&n);
  if (c == '"')
    c = get_copy(&n);
  macro *m = new macro;
  while (c != '\n' && c != EOF) {
    if (c == 0)
      m->append(n);
    else
      m->append((unsigned char)c);
    c = get_copy(&n);
  }
  m = ci->setx_macro(m, mode);
  if (m)
    delete m;
  tok.next();
}

void define_character()
{
  do_define_character(CHAR_NORMAL);
}

void define_fallback_character()
{
  do_define_character(CHAR_FALLBACK);
}

void define_special_character()
{
  do_define_character(CHAR_SPECIAL);
}

static void remove_character()
{
  tok.skip();
  while (!tok.is_newline() && !tok.is_eof()) {
    if (!tok.is_space() && !tok.is_tab()) {
      charinfo *ci = tok.get_char(true /* required */);
      if (!ci)
	break;
      macro *m = ci->set_macro(0);
      if (m)
	delete m;
    }
    tok.next();
  }
  skip_line();
}

static void interpolate_string(symbol nm)
{
  request_or_macro *p = lookup_request(nm);
  macro *m = p->to_macro();
  if (!m)
    error("cannot interpolate request '%1'", nm.contents());
  else {
    if (m->is_string()) {
      string_iterator *si = new string_iterator(*m, "string", nm);
      input_stack::push(si);
     }
    else {
      // if a macro is called as a string, \$0 doesn't get changed
      macro_iterator *mi = new macro_iterator(input_stack::get_macro_name(),
					      *m, "string", 1);
      input_stack::push(mi);
    }
  }
}

static void interpolate_string_with_args(symbol nm)
{
  request_or_macro *p = lookup_request(nm);
  macro *m = p->to_macro();
  if (!m)
    error("cannot interpolate request '%1'", nm.contents());
  else {
    macro_iterator *mi = new macro_iterator(nm, *m);
    decode_string_args(mi);
    input_stack::push(mi);
  }
}

static void interpolate_arg(symbol nm)
{
  const char *s = nm.contents();
  if (!s || *s == '\0')
    copy_mode_error("missing positional argument number");
  else if (s[1] == 0 && csdigit(s[0]))
    input_stack::push(input_stack::get_arg(s[0] - '0'));
  else if (s[0] == '*' && s[1] == '\0') {
    int limit = input_stack::nargs();
    string args;
    for (int i = 1; i <= limit; i++) {
      input_iterator *p = input_stack::get_arg(i);
      int c;
      while ((c = p->get(0)) != EOF)
	if (c != DOUBLE_QUOTE)
	  args += c;
      if (i != limit)
	args += ' ';
      delete p;
    }
    if (limit > 0) {
      args += '\0';
      input_stack::push(make_temp_iterator(args.contents()));
    }
  }
  else if (s[0] == '@' && s[1] == '\0') {
    int limit = input_stack::nargs();
    string args;
    for (int i = 1; i <= limit; i++) {
      args += '"';
      args += char(BEGIN_QUOTE);
      input_iterator *p = input_stack::get_arg(i);
      int c;
      while ((c = p->get(0)) != EOF)
	if (c != DOUBLE_QUOTE)
	  args += c;
      args += char(END_QUOTE);
      args += '"';
      if (i != limit)
	args += ' ';
      delete p;
    }
    if (limit > 0) {
      args += '\0';
      input_stack::push(make_temp_iterator(args.contents()));
    }
  }
  else if (s[0] == '^' && s[1] == '\0') {
    int limit = input_stack::nargs();
    string args;
    int c = input_stack::peek();
    for (int i = 1; i <= limit; i++) {
      input_iterator *p = input_stack::get_arg(i);
      while ((c = p->get(0)) != EOF) {
	if (c == DOUBLE_QUOTE)
	  c = '"';
	args += c;
      }
      if (input_stack::space_follows_arg(i))
	args += ' ';
      delete p;
    }
    if (limit > 0) {
      args += '\0';
      input_stack::push(make_temp_iterator(args.contents()));
    }
  }
  else {
    const char *p;
    for (p = s; *p && csdigit(*p); p++)
      ;
    if (*p)
      copy_mode_error("invalid positional argument number '%1'", s);
    else
      input_stack::push(input_stack::get_arg(atoi(s)));
  }
}

void handle_first_page_transition()
{
  push_token(tok);
  topdiv->begin_page();
}

// We push back a token by wrapping it up in a token_node, and
// wrapping that up in a string_iterator.

static void push_token(const token &t)
{
  macro m;
  m.append(new token_node(t));
  input_stack::push(new string_iterator(m));
}

void push_page_ejector()
{
  static char buf[2] = { PAGE_EJECTOR, '\0' };
  input_stack::push(make_temp_iterator(buf));
}

void handle_initial_request(unsigned char code)
{
  char buf[2];
  buf[0] = code;
  buf[1] = '\0';
  macro mac;
  mac.append(new token_node(tok));
  input_stack::push(new string_iterator(mac));
  input_stack::push(make_temp_iterator(buf));
  topdiv->begin_page();
  tok.next();
}

void handle_initial_title()
{
  handle_initial_request(TITLE_REQUEST);
}

void do_define_macro(define_mode mode, calling_mode calling, comp_mode comp)
{
  symbol nm, term, dot_symbol(".");
  if (calling == CALLING_INDIRECT) {
    symbol temp1 = get_name(true /* required */);
    if (temp1.is_null()) {
      skip_line();
      return;
    }
    symbol temp2 = get_name();
    input_stack::push(make_temp_iterator("\n"));
    if (!temp2.is_null()) {
      interpolate_string(temp2);
      input_stack::push(make_temp_iterator(" "));
    }
    interpolate_string(temp1);
    input_stack::push(make_temp_iterator(" "));
    tok.next();
  }
  if (mode == DEFINE_NORMAL || mode == DEFINE_APPEND) {
    nm = get_name(true /* required */);
    if (nm.is_null()) {
      skip_line();
      return;
    }
  }
  term = get_name();	// the request that terminates the definition
  if (term.is_null())
    term = dot_symbol;
  while (!tok.is_newline() && !tok.is_eof())
    tok.next();
  const char *start_filename;
  int start_lineno;
  int have_start_location = input_stack::get_location(0, &start_filename,
						      &start_lineno);
  node *n;
  // doing this here makes the line numbers come out right
  int c = get_copy(&n, true /* is defining*/);
  macro mac;
  macro *mm = 0;
  if (mode == DEFINE_NORMAL || mode == DEFINE_APPEND) {
    request_or_macro *rm =
      (request_or_macro *)request_dictionary.lookup(nm);
    if (rm)
      mm = rm->to_macro();
    if (mm && mode == DEFINE_APPEND)
      mac = *mm;
  }
  int bol = 1;
  if (comp == COMP_DISABLE)
    mac.append(PUSH_GROFF_MODE);
  else if (comp == COMP_ENABLE)
    mac.append(PUSH_COMP_MODE);
  for (;;) {
    if (c == '\n')
      mac.clear_string_flag();
    while (c == ESCAPE_NEWLINE) {
      if (mode == DEFINE_NORMAL || mode == DEFINE_APPEND)
	mac.append(c);
      c = get_copy(&n, true /* is defining */);
    }
    if (bol && c == '.') {
      const char *s = term.contents();
      int d = 0;
      // see if it matches term
      int i = 0;
      if (s[0] != 0) {
	while ((d = get_copy(&n)) == ' ' || d == '\t')
	  ;
	if ((unsigned char)s[0] == d) {
	  for (i = 1; s[i] != 0; i++) {
	    d = get_copy(&n);
	    if ((unsigned char)s[i] != d)
	      break;
	  }
	}
      }
      if (s[i] == 0
	  && ((i == 2 && compatible_flag)
	      || (d = get_copy(&n)) == ' '
	      || d == '\n')) {	// we found it
	if (d == '\n')
	  tok.make_newline();
	else
	  tok.make_space();
	if (mode == DEFINE_APPEND || mode == DEFINE_NORMAL) {
	  if (!mm) {
	    mm = new macro;
	    request_dictionary.define(nm, mm);
	  }
	  if (comp == COMP_DISABLE || comp == COMP_ENABLE)
	    mac.append(POP_GROFFCOMP_MODE);
	  *mm = mac;
	}
	if (term != dot_symbol) {
	  ignoring = 0;
	  interpolate_macro(term);
	}
	else
	  skip_line();
	return;
      }
      if (mode == DEFINE_APPEND || mode == DEFINE_NORMAL) {
	mac.append(c);
	for (int j = 0; j < i; j++)
	  mac.append(s[j]);
      }
      c = d;
    }
    if (c == EOF) {
      if (mode == DEFINE_NORMAL || mode == DEFINE_APPEND) {
	if (have_start_location)
	  error_with_file_and_line(start_filename, start_lineno,
				   "end of file while defining macro '%1'",
				   nm.contents());
	else
	  error("end of file while defining macro '%1'", nm.contents());
      }
      else {
	if (have_start_location)
	  error_with_file_and_line(start_filename, start_lineno,
				   "end of file while ignoring input lines");
	else
	  error("end of file while ignoring input lines");
      }
      tok.next();
      return;
    }
    if (mode == DEFINE_NORMAL || mode == DEFINE_APPEND) {
      if (c == 0)
	mac.append(n);
      else
	mac.append(c);
    }
    bol = (c == '\n');
    c = get_copy(&n, true /* is defining */);
  }
}

void define_macro()
{
  do_define_macro(DEFINE_NORMAL, CALLING_NORMAL,
		  compatible_flag ? COMP_ENABLE : COMP_IGNORE);
}

void define_nocomp_macro()
{
  do_define_macro(DEFINE_NORMAL, CALLING_NORMAL, COMP_DISABLE);
}

void define_indirect_macro()
{
  do_define_macro(DEFINE_NORMAL, CALLING_INDIRECT,
		  compatible_flag ? COMP_ENABLE : COMP_IGNORE);
}

void define_indirect_nocomp_macro()
{
  do_define_macro(DEFINE_NORMAL, CALLING_INDIRECT, COMP_DISABLE);
}

void append_macro()
{
  do_define_macro(DEFINE_APPEND, CALLING_NORMAL,
		  compatible_flag ? COMP_ENABLE : COMP_IGNORE);
}

void append_nocomp_macro()
{
  do_define_macro(DEFINE_APPEND, CALLING_NORMAL, COMP_DISABLE);
}

void append_indirect_macro()
{
  do_define_macro(DEFINE_APPEND, CALLING_INDIRECT,
		  compatible_flag ? COMP_ENABLE : COMP_IGNORE);
}

void append_indirect_nocomp_macro()
{
  do_define_macro(DEFINE_APPEND, CALLING_INDIRECT, COMP_DISABLE);
}

void ignore()
{
  ignoring = 1;
  do_define_macro(DEFINE_IGNORE, CALLING_NORMAL, COMP_IGNORE);
  ignoring = 0;
}

void remove_macro()
{
  for (;;) {
    symbol s = get_name();
    if (s.is_null())
      break;
    request_dictionary.remove(s);
  }
  skip_line();
}

void rename_macro()
{
  symbol s1 = get_name(true /* required */);
  if (!s1.is_null()) {
    symbol s2 = get_name(true /* required */);
    if (!s2.is_null())
      request_dictionary.rename(s1, s2);
  }
  skip_line();
}

void alias_macro()
{
  symbol s1 = get_name(true /* required */);
  if (!s1.is_null()) {
    symbol s2 = get_name(true /* required */);
    if (!s2.is_null()) {
      if (!request_dictionary.alias(s1, s2))
	warning(WARN_MAC, "macro '%1' not defined", s2.contents());
    }
  }
  skip_line();
}

void chop_macro()
{
  symbol s = get_name(true /* required */);
  if (!s.is_null()) {
    request_or_macro *p = lookup_request(s);
    macro *m = p->to_macro();
    if (!m)
      error("cannot chop request");
    else if (m->empty())
      error("cannot chop empty macro");
    else {
      int have_restore = 0;
      // we have to check for additional save/restore pairs which could be
      // there due to empty am1 requests.
      for (;;) {
	if (m->get(m->len - 1) != POP_GROFFCOMP_MODE)
	  break;
	have_restore = 1;
	m->len -= 1;
	if (m->get(m->len - 1) != PUSH_GROFF_MODE
	    && m->get(m->len - 1) != PUSH_COMP_MODE)
	  break;
	have_restore = 0;
	m->len -= 1;
	if (m->len == 0)
	  break;
      }
      if (m->len == 0)
	error("cannot chop empty macro");
      else {
	if (have_restore)
	  m->set(POP_GROFFCOMP_MODE, m->len - 1);
	else
	  m->len -= 1;
      }
    }
  }
  skip_line();
}

enum case_xform_mode { STRING_UPCASE, STRING_DOWNCASE };

// Case-transform each byte of the string argument's contents.
void do_string_case_transform(case_xform_mode mode)
{
  assert((mode == STRING_DOWNCASE) || (mode == STRING_UPCASE));
  symbol s = get_name(true /* required */);
  if (s.is_null()) {
    skip_line();
    return;
  }
  request_or_macro *p = lookup_request(s);
  macro *m = p->to_macro();
  if (!m) {
    error("cannot apply string case transformation to a request ('%1')",
	  s.contents());
    skip_line();
    return;
  }
  string_iterator iter1(*m);
  macro *mac = new macro;
  int len = m->macro::length();
  for (int l = 0; l < len; l++) {
    int nc, c = iter1.get(0);
    if (c == PUSH_GROFF_MODE
	|| c == PUSH_COMP_MODE
	|| c == POP_GROFFCOMP_MODE)
      nc = c;
    else if (c == EOF)
      break;
    else
      if (mode == STRING_DOWNCASE)
	nc = tolower(c);
      else
	nc = toupper(c);
    mac->append(nc);
  }
  request_dictionary.define(s, mac);
  tok.next();
}

// Uppercase-transform each byte of the string argument's contents.
void stringdown_request() {
  do_string_case_transform(STRING_DOWNCASE);
}

// Lowercase-transform each byte of the string argument's contents.
void stringup_request() {
  do_string_case_transform(STRING_UPCASE);
}

void substring_request()
{
  int start;				// 0, 1, ..., n-1  or  -1, -2, ...
  symbol s = get_name(true /* required */);
  if (!s.is_null() && get_integer(&start)) {
    request_or_macro *p = lookup_request(s);
    macro *m = p->to_macro();
    if (!m)
      error("cannot apply 'substring' on a request");
    else {
      int end = -1;
      if (!has_arg() || get_integer(&end)) {
	int real_length = 0;			// 1, 2, ..., n
	string_iterator iter1(*m);
	for (int l = 0; l < m->len; l++) {
	  int c = iter1.get(0);
	  if (c == PUSH_GROFF_MODE
	      || c == PUSH_COMP_MODE
	      || c == POP_GROFFCOMP_MODE)
	    continue;
	  if (c == EOF)
	    break;
	  real_length++;
	}
	if (start < 0)
	  start += real_length;
	if (end < 0)
	  end += real_length;
	if (start > end) {
	  int tem = start;
	  start = end;
	  end = tem;
	}
	if (start >= real_length || end < 0) {
	  warning(WARN_RANGE,
		  "start and end index of substring out of range");
	  m->len = 0;
	  if (m->p) {
	    if (--(m->p->count) <= 0)
	      delete m->p;
	    m->p = 0;
	  }
	  skip_line();
	  return;
	}
	if (start < 0) {
	  warning(WARN_RANGE,
		  "start index of substring out of range, set to 0");
	  start = 0;
	}
	if (end >= real_length) {
	  warning(WARN_RANGE,
		  "end index of substring out of range, set to string length");
	  end = real_length - 1;
	}
	// now extract the substring
	string_iterator iter(*m);
	int i;
	for (i = 0; i < start; i++) {
	  int c = iter.get(0);
	  while (c == PUSH_GROFF_MODE
		 || c == PUSH_COMP_MODE
		 || c == POP_GROFFCOMP_MODE)
	    c = iter.get(0);
	  if (c == EOF)
	    break;
	}
	macro mac;
	for (; i <= end; i++) {
	  node *nd = 0;		// pacify compiler
	  int c = iter.get(&nd);
	  while (c == PUSH_GROFF_MODE
		 || c == PUSH_COMP_MODE
		 || c == POP_GROFFCOMP_MODE)
	    c = iter.get(0);
	  if (c == EOF)
	    break;
	  if (c == 0)
	    mac.append(nd);
	  else
	    mac.append((unsigned char)c);
	}
	*m = mac;
      }
    }
  }
  skip_line();
}

void length_request()
{
  symbol ret;
  ret = get_name(true /* required */);
  if (ret.is_null()) {
    skip_line();
    return;
  }
  int c;
  node *n;
  if (tok.is_newline())
    c = '\n';
  else if (tok.is_tab())
    c = '\t';
  else if (!tok.is_space()) {
    error("bad string definition");
    skip_line();
    return;
  }
  else
    c = get_copy(&n);
  while (c == ' ')
    c = get_copy(&n);
  if (c == '"')
    c = get_copy(&n);
  int len = 0;
  while (c != '\n' && c != EOF) {
    ++len;
    c = get_copy(&n);
  }
  reg *r = (reg*)register_dictionary.lookup(ret);
  if (r)
    r->set_value(len);
  else
    set_number_reg(ret, len);
  tok.next();
}

void asciify_macro()
{
  symbol s = get_name(true /* required */);
  if (!s.is_null()) {
    request_or_macro *p = lookup_request(s);
    macro *m = p->to_macro();
    if (!m)
      error("cannot asciify request");
    else {
      macro am;
      string_iterator iter(*m);
      for (;;) {
	node *nd = 0;		// pacify compiler
	int c = iter.get(&nd);
	if (c == EOF)
	  break;
	if (c != 0)
	  am.append(c);
	else
	  nd->asciify(&am);
      }
      *m = am;
    }
  }
  skip_line();
}

void unformat_macro()
{
  symbol s = get_name(true /* required */);
  if (!s.is_null()) {
    request_or_macro *p = lookup_request(s);
    macro *m = p->to_macro();
    if (!m)
      error("cannot unformat request");
    else {
      macro am;
      string_iterator iter(*m);
      for (;;) {
	node *nd = 0;		// pacify compiler
	int c = iter.get(&nd);
	if (c == EOF)
	  break;
	if (c != 0)
	  am.append(c);
	else {
	  if (nd->set_unformat_flag())
	    am.append(nd);
	}
      }
      *m = am;
    }
  }
  skip_line();
}

static void interpolate_environment_variable(symbol nm)
{
  const char *s = getenv(nm.contents());
  if (s && *s)
    input_stack::push(make_temp_iterator(s));
}

void interpolate_number_reg(symbol nm, int inc)
{
  reg *r = lookup_number_reg(nm);
  if (inc < 0)
    r->decrement();
  else if (inc > 0)
    r->increment();
  input_stack::push(make_temp_iterator(r->get_string()));
}

static void interpolate_number_format(symbol nm)
{
  reg *r = (reg *)register_dictionary.lookup(nm);
  if (r)
    input_stack::push(make_temp_iterator(r->get_format()));
}

static int get_delim_number(units *n, unsigned char si, int prev_value)
{
  token start;
  start.next();
  if (start.usable_as_delimiter(true /* report error */)) {
    tok.next();
    if (get_number(n, si, prev_value)) {
      if (start != tok)
	warning(WARN_DELIM, "closing delimiter does not match");
      return 1;
    }
  }
  return 0;
}

static int get_delim_number(units *n, unsigned char si)
{
  token start;
  start.next();
  if (start.usable_as_delimiter(true /* report error */)) {
    tok.next();
    if (get_number(n, si)) {
      if (start != tok)
	warning(WARN_DELIM, "closing delimiter does not match");
      return 1;
    }
  }
  return 0;
}

static int get_line_arg(units *n, unsigned char si, charinfo **cp)
{
  token start;
  start.next();
  int start_level = input_stack::get_level();
  if (!start.usable_as_delimiter(true /* report error */))
    return 0;
  tok.next();
  if (get_number(n, si)) {
    if (tok.is_dummy() || tok.is_transparent_dummy())
      tok.next();
    if (!(start == tok && input_stack::get_level() == start_level)) {
      *cp = tok.get_char(true /* required */);
      tok.next();
    }
    if (!(start == tok && input_stack::get_level() == start_level))
      warning(WARN_DELIM, "closing delimiter does not match");
    return 1;
  }
  return 0;
}

static bool read_size(int *x)
{
  tok.next();
  int c = tok.ch();
  int inc = 0;
  if (c == '-') {
    inc = -1;
    tok.next();
    c = tok.ch();
  }
  else if (c == '+') {
    inc = 1;
    tok.next();
    c = tok.ch();
  }
  int val = 0;		// pacify compiler
  bool contains_invalid_digit = false;
  if (c == '(') {
    tok.next();
    c = tok.ch();
    if (!inc) {
      // allow an increment either before or after the left parenthesis
      if (c == '-') {
	inc = -1;
	tok.next();
	c = tok.ch();
      }
      else if (c == '+') {
	inc = 1;
	tok.next();
	c = tok.ch();
      }
    }
    if (!csdigit(c))
      contains_invalid_digit = true;
    else {
      val = c - '0';
      tok.next();
      c = tok.ch();
      if (!csdigit(c))
	contains_invalid_digit = true;
      else {
	val = val*10 + (c - '0');
	val *= sizescale;
      }
    }
  }
  else if (csdigit(c)) {
    val = c - '0';
    if (compatible_flag && !inc && c != '0' && c < '4') {
      // Support legacy \sNN syntax.
      tok.next();
      c = tok.ch();
      if (!csdigit(c))
	contains_invalid_digit = true;
      else {
	val = val*10 + (c - '0');
	error("ambiguous type size in escape sequence; rewrite to use"
	      " '%1s(%2' or similar", static_cast<char>(escape_char),
	      val);
      }
    }
    val *= sizescale;
  }
  else if (!tok.usable_as_delimiter(true /* report error */))
    return false;
  else {
    token start(tok);
    tok.next();
    c = tok.ch();
    if (!inc && (c == '-' || c == '+')) {
      inc = c == '+' ? 1 : -1;
      tok.next();
    }
    if (!get_number(&val, 'z'))
      return false;
    if (!(start.ch() == '[' && tok.ch() == ']') && start != tok) {
      if (start.ch() == '[')
	error("missing ']' in type size escape sequence");
      else
	error("missing closing delimiter in type size escape sequence");
      return false;
    }
  }
  if (contains_invalid_digit) {
    if (c)
      error("expected valid digit in type size escape sequence, got %1",
	    input_char_description(c));
    else
      error("invalid digit in type size escape sequence");
    return false;
  }
  else {
    switch (inc) {
    case 0:
      if (val == 0) {
	// special case -- point size 0 means "revert to previous size"
	*x = 0;
	return true;
      }
      *x = val;
      break;
    case 1:
      *x = curenv->get_requested_point_size() + val;
      break;
    case -1:
      *x = curenv->get_requested_point_size() - val;
      break;
    default:
      assert(0);
    }
    if (*x <= 0) {
      warning(WARN_RANGE,
	      "type size escape sequence results in non-positive size"
	      " %1u; setting it to 1u", *x);
      *x = 1;
    }
    return true;
  }
}

static symbol get_delim_name()
{
  token start;
  start.next();
  if (start.is_eof()) {
    error("end of input at start of delimited name");
    return NULL_SYMBOL;
  }
  if (start.is_newline()) {
    error("can't delimit name with a newline");
    return NULL_SYMBOL;
  }
  int start_level = input_stack::get_level();
  char abuf[ABUF_SIZE];
  char *buf = abuf;
  int buf_size = ABUF_SIZE;
  int i = 0;
  for (;;) {
    if (i + 1 > buf_size) {
      if (buf == abuf) {
	buf = new char[ABUF_SIZE*2];
	memcpy(buf, abuf, buf_size);
	buf_size = ABUF_SIZE*2;
      }
      else {
	char *old_buf = buf;
	buf = new char[buf_size*2];
	memcpy(buf, old_buf, buf_size);
	buf_size *= 2;
	delete[] old_buf;
      }
    }
    tok.next();
    if (tok == start
	&& (compatible_flag || input_stack::get_level() == start_level))
      break;
    if ((buf[i] = tok.ch()) == 0) {
      error("missing delimiter (got %1)", tok.description());
      if (buf != abuf)
	delete[] buf;
      return NULL_SYMBOL;
    }
    i++;
  }
  buf[i] = '\0';
  if (buf == abuf) {
    if (i == 0) {
      error("empty delimited name");
      return NULL_SYMBOL;
    }
    else
      return symbol(buf);
  }
  else {
    symbol s(buf);
    delete[] buf;
    return s;
  }
}

// Implement \R

static void do_register()
{
  token start;
  start.next();
  if (!start.usable_as_delimiter(true /* report error */))
    return;
  tok.next();
  symbol nm = get_long_name(true /* required */);
  if (nm.is_null())
    return;
  while (tok.is_space())
    tok.next();
  reg *r = (reg *)register_dictionary.lookup(nm);
  int prev_value;
  if (!r || !r->get_value(&prev_value))
    prev_value = 0;
  int val;
  if (!get_number(&val, 'u', prev_value))
    return;
  if (start != tok)
    warning(WARN_DELIM, "closing delimiter does not match");
  if (r)
    r->set_value(val);
  else
    set_number_reg(nm, val);
}

// this implements the \w escape sequence

static void do_width()
{
  int start_level = input_stack::get_level();
  token start;
  start.next();
  environment env(curenv);
  environment *oldenv = curenv;
  curenv = &env;
  for (;;) {
    tok.next();
    if (tok.is_newline() || tok.is_eof()) {
      if (tok != start)
	warning(WARN_DELIM, "missing closing delimiter in"
		" width computation escape sequence (got %1)",
		tok.description());
      // Synthesize an input line ending.
      input_stack::push(make_temp_iterator("\n"));
      break;
    }
    if (tok == start
	&& (compatible_flag || input_stack::get_level() == start_level))
      break;
    tok.process();
  }
  env.wrap_up_tab();
  units x = env.get_input_line_position().to_units();
  input_stack::push(make_temp_iterator(i_to_a(x)));
  env.width_registers();
  curenv = oldenv;
  have_input = 0;
}

charinfo *page_character;

void set_page_character()
{
  page_character = get_optional_char();
  skip_line();
}

static const symbol percent_symbol("%");

void read_title_parts(node **part, hunits *part_width)
{
  tok.skip();
  if (tok.is_newline() || tok.is_eof())
    return;
  token start(tok);
  int start_level = input_stack::get_level();
  tok.next();
  for (int i = 0; i < 3; i++) {
    while (!tok.is_newline() && !tok.is_eof()) {
      if (tok == start
	  && (compatible_flag || input_stack::get_level() == start_level)) {
	tok.next();
	break;
      }
      if (page_character != 0 && tok.get_char() == page_character)
	interpolate_number_reg(percent_symbol, 0);
      else
	tok.process();
      tok.next();
    }
    curenv->wrap_up_tab();
    part_width[i] = curenv->get_input_line_position();
    part[i] = curenv->extract_output_line();
  }
  while (!tok.is_newline() && !tok.is_eof())
    tok.next();
}

class non_interpreted_node : public node {
  macro mac;
public:
  non_interpreted_node(const macro &);
  int interpret(macro *);
  node *copy();
  int ends_sentence();
  int same(node *);
  const char *type();
  int force_tprint();
  int is_tag();
};

non_interpreted_node::non_interpreted_node(const macro &m) : mac(m)
{
}

int non_interpreted_node::ends_sentence()
{
  return 2;
}

int non_interpreted_node::same(node *nd)
{
  return mac == ((non_interpreted_node *)nd)->mac;
}

const char *non_interpreted_node::type()
{
  return "non_interpreted_node";
}

int non_interpreted_node::force_tprint()
{
  return 0;
}

int non_interpreted_node::is_tag()
{
  return 0;
}

node *non_interpreted_node::copy()
{
  return new non_interpreted_node(mac);
}

int non_interpreted_node::interpret(macro *m)
{
  string_iterator si(mac);
  node *n = 0;		// pacify compiler
  for (;;) {
    int c = si.get(&n);
    if (c == EOF)
      break;
    if (c == 0)
      m->append(n);
    else
      m->append(c);
  }
  return 1;
}

static node *do_non_interpreted()
{
  node *n;
  int c;
  macro mac;
  while ((c = get_copy(&n)) != ESCAPE_QUESTION && c != EOF && c != '\n')
    if (c == 0)
      mac.append(n);
    else
      mac.append(c);
  if (c == EOF || c == '\n') {
    error("unterminated transparent embedding escape sequence");
    return 0;
  }
  return new non_interpreted_node(mac);
}

static void encode_char(macro *mac, char c)
{
  if (c == '\0') {
    if (tok.is_stretchable_space()
	     || tok.is_unstretchable_space())
      mac->append(' ');
    else if (tok.is_special()) {
      const char *sc;
      if (font::use_charnames_in_special) {
	charinfo *ci = tok.get_char(true /* required */);
	sc = ci->get_symbol()->contents();
      }
      else
	sc = tok.get_char()->get_symbol()->contents();
      if (strcmp("-", sc) == 0)
	mac->append('-');
      else if (strcmp("aq", sc) == 0)
	mac->append('\'');
      else if (strcmp("dq", sc) == 0)
	mac->append('"');
      else if (strcmp("ga", sc) == 0)
	mac->append('`');
      else if (strcmp("ha", sc) == 0)
	mac->append('^');
      else if (strcmp("rs", sc) == 0)
	mac->append('\\');
      else if (strcmp("ti", sc) == 0)
	mac->append('~');
      else {
	if (font::use_charnames_in_special) {
	  if (sc[0] != (char)0) {
	    mac->append('\\');
	    mac->append('[');
	    int i = 0;
	    while (sc[i] != (char)0) {
	      mac->append(sc[i]);
	      i++;
	    }
	    mac->append(']');
	  }
	  else
	      error("special character '%1' cannot be used within"
		    " device control escape sequence", sc);
	}
      }
    }
    else if (!(tok.is_hyphen_indicator()
	       || tok.is_dummy()
	       || tok.is_transparent_dummy()
	       || tok.is_zero_width_break()))
      error("%1 is invalid within device control escape sequence",
	    tok.description());
  }
  else {
    if ((font::use_charnames_in_special) && (c == '\\')) {
      /*
       * add escape escape sequence
       */
      mac->append(c);
    }
    mac->append(c);
  }
}

static node *do_special()
{
  int start_level = input_stack::get_level();
  token start;
  start.next();
  macro mac;
  for (;;) {
    tok.next();
    if (tok.is_newline()) {
      input_stack::push(make_temp_iterator("\n"));
      break;
    }
    if (tok.is_eof()) {
      warning(WARN_DELIM, "missing closing delimiter in device control"
	      " escape sequence (got %1)", tok.description());
      // Synthesize an input line ending.
      input_stack::push(make_temp_iterator("\n"));
      break;
    }
    if (tok == start
	&& (compatible_flag || input_stack::get_level() == start_level))
      break;
    unsigned char c;
    if (tok.is_space())
      c = ' ';
    else if (tok.is_tab())
      c = '\t';
    else if (tok.is_leader())
      c = '\001';
    else if (tok.is_backspace())
      c = '\b';
    else
      c = tok.ch();
    encode_char(&mac, c);
  }
  return new special_node(mac);
}

void device_request()
{
  if (!tok.is_newline() && !tok.is_eof()) {
    int c;
    macro mac;
    for (;;) {
      c = get_copy(0);
      if (c == '"') {
	c = get_copy(0);
	break;
      }
      if (c != ' ' && c != '\t')
	break;
    }
    for (; c != '\n' && c != EOF; c = get_copy(0))
      mac.append(c);
    curenv->add_node(new special_node(mac));
  }
  tok.next();
}

void device_macro_request()
{
  symbol s = get_name(true /* required */);
  if (!(s.is_null() || s.is_empty())) {
    request_or_macro *p = lookup_request(s);
    macro *m = p->to_macro();
    if (m)
      curenv->add_node(new special_node(*m));
    else
      error("can't transparently throughput a request");
  }
  skip_line();
}

void output_request()
{
  if (!tok.is_newline() && !tok.is_eof()) {
    int c;
    for (;;) {
      c = get_copy(0);
      if (c == '"') {
	c = get_copy(0);
	break;
      }
      if (c != ' ' && c != '\t')
	break;
    }
    for (; c != '\n' && c != EOF; c = get_copy(0))
      topdiv->transparent_output(c);
    topdiv->transparent_output('\n');
  }
  tok.next();
}

extern int image_no;		// from node.cpp

static node *do_suppress(symbol nm)
{
  if (nm.is_null() || nm.is_empty()) {
    error("output suppression escape sequence requires an argument");
    return 0;
  }
  const char *s = nm.contents();
  switch (*s) {
  case '0':
    if (begin_level == 0)
      // suppress generation of glyphs
      return new suppress_node(0, 0);
    break;
  case '1':
    if (begin_level == 0)
      // enable generation of glyphs
      return new suppress_node(1, 0);
    break;
  case '2':
    if (begin_level == 0)
      return new suppress_node(1, 1);
    break;
  case '3':
    have_input = 1;
    begin_level++;
    break;
  case '4':
    have_input = 1;
    begin_level--;
    break;
  case '5':
    {
      s++;			// move over '5'
      char position = *s;
      if (*s == (char)0) {
	error("missing position and filename in output suppression"
	      " escape sequence");
	return 0;
      }
      if (!(position == 'l'
	    || position == 'r'
	    || position == 'c'
	    || position == 'i')) {
	error("expected position 'l', 'r', 'c', or 'i' in output"
	      " suppression escape sequence, got '%1'", position);
	return 0;
      }
      s++;			// onto image name
      if (s == (char *)0) {
	error("missing image name in output suppression escape"
	      " sequence");
	return 0;
      }
      image_no++;
      if (begin_level == 0)
	return new suppress_node(symbol(s), position, image_no);
      else
	have_input = 1;
    }
    break;
  default:
    error("invalid argument '%1' to output suppression escape sequence",
	  *s);
  }
  return 0;
}

void special_node::tprint(troff_output_file *out)
{
  tprint_start(out);
  string_iterator iter(mac);
  for (;;) {
    int c = iter.get(0);
    if (c == EOF)
      break;
    for (const char *s = ::asciify(c); *s; s++)
      tprint_char(out, *s);
  }
  tprint_end(out);
}

int get_file_line(const char **filename, int *lineno)
{
  return input_stack::get_location(0, filename, lineno);
}

void line_file()
{
  int n;
  if (get_integer(&n)) {
    const char *filename = 0;
    if (has_arg()) {
      symbol s = get_long_name();
      filename = s.contents();
    }
    (void)input_stack::set_location(filename, n-1);
  }
  skip_line();
}

static int nroff_mode = 0;

static void nroff_request()
{
  nroff_mode = 1;
  skip_line();
}

static void troff_request()
{
  nroff_mode = 0;
  skip_line();
}

static void skip_alternative()
{
  int level = 0;
  // ensure that ".if 0\{" works as expected
  if (tok.is_left_brace())
    level++;
  int c;
  for (;;) {
    c = input_stack::get(0);
    if (c == EOF)
      break;
    if (c == ESCAPE_LEFT_BRACE)
      ++level;
    else if (c == ESCAPE_RIGHT_BRACE)
      --level;
    else if (c == escape_char && escape_char > 0)
      switch(input_stack::get(0)) {
      case '{':
	++level;
	break;
      case '}':
	--level;
	break;
      case '"':
	while ((c = input_stack::get(0)) != '\n' && c != EOF)
	  ;
      }
    /*
      Note that the level can properly be < 0, e.g.

	.if 1 \{\
	.if 0 \{\
	.\}\}

      So don't give an error message in this case.
    */
    if (level <= 0 && c == '\n')
      break;
  }
  tok.next();
}

static void begin_alternative()
{
  while (tok.is_space() || tok.is_left_brace())
    tok.next();
}

void nop_request()
{
  while (tok.is_space())
    tok.next();
}

static int_stack if_else_stack;

int do_if_request()
{
  int invert = 0;
  while (tok.is_space())
    tok.next();
  while (tok.ch() == '!') {
    tok.next();
    invert = !invert;
  }
  int result;
  unsigned char c = tok.ch();
  if (compatible_flag)
    switch (c) {
    case 'F':
    case 'S':
    case 'c':
    case 'd':
    case 'm':
    case 'r':
      warning(WARN_SYNTAX,
	      "conditional operator '%1' used in compatibility mode",
	      c);
      break;
    default:
      break;
    }
  if (c == 't') {
    tok.next();
    result = !nroff_mode;
  }
  else if (c == 'n') {
    tok.next();
    result = nroff_mode;
  }
  else if (c == 'v') {
    tok.next();
    result = 0;
  }
  else if (c == 'o') {
    result = (topdiv->get_page_number() & 1);
    tok.next();
  }
  else if (c == 'e') {
    result = !(topdiv->get_page_number() & 1);
    tok.next();
  }
  else if (c == 'd' || c == 'r') {
    tok.next();
    symbol nm = get_name(true /* required */);
    if (nm.is_null()) {
      skip_alternative();
      return 0;
    }
    result = (c == 'd'
	      ? request_dictionary.lookup(nm) != 0
	      : register_dictionary.lookup(nm) != 0);
  }
  else if (c == 'm') {
    tok.next();
    symbol nm = get_long_name(true /* required */);
    if (nm.is_null()) {
      skip_alternative();
      return 0;
    }
    result = (nm == default_symbol
	      || color_dictionary.lookup(nm) != 0);
  }
  else if (c == 'c') {
    tok.next();
    tok.skip();
    charinfo *ci = tok.get_char(true /* required */);
    if (ci == 0) {
      skip_alternative();
      return 0;
    }
    result = character_exists(ci, curenv);
    tok.next();
  }
  else if (c == 'F') {
    tok.next();
    symbol nm = get_long_name(true /* required */);
    if (nm.is_null()) {
      skip_alternative();
      return 0;
    }
    result = check_font(curenv->get_family()->nm, nm);
  }
  else if (c == 'S') {
    tok.next();
    symbol nm = get_long_name(true /* required */);
    if (nm.is_null()) {
      skip_alternative();
      return 0;
    }
    result = check_style(nm);
  }
  else if (tok.is_space())
    result = 0;
  else if (tok.usable_as_delimiter()) {
    token delim = tok;
    int delim_level = input_stack::get_level();
    environment env1(curenv);
    environment env2(curenv);
    environment *oldenv = curenv;
    curenv = &env1;
    suppress_push = 1;
    for (int i = 0; i < 2; i++) {
      for (;;) {
	tok.next();
	if (tok.is_newline() || tok.is_eof()) {
	  warning(WARN_DELIM, "missing closing delimiter in output"
		  " comparison operator (got %1)", tok.description());
	  tok.next();
	  curenv = oldenv;
	  return 0;
	}
	if (tok == delim
	    && (compatible_flag || input_stack::get_level() == delim_level))
	  break;
	tok.process();
      }
      curenv = &env2;
    }
    node *n1 = env1.extract_output_line();
    node *n2 = env2.extract_output_line();
    result = same_node_list(n1, n2);
    delete_node_list(n1);
    delete_node_list(n2);
    curenv = oldenv;
    have_input = 0;
    suppress_push = 0;
    tok.next();
  }
  else {
    units n;
    if (!get_number(&n, 'u')) {
      skip_alternative();
      return 0;
    }
    else
      result = n > 0;
  }
  if (invert)
    result = !result;
  if (result)
    begin_alternative();
  else
    skip_alternative();
  return result;
}

void if_else_request()
{
  if_else_stack.push(do_if_request());
}

void if_request()
{
  do_if_request();
}

void else_request()
{
  if (if_else_stack.is_empty()) {
    warning(WARN_EL, "unbalanced 'el' request");
    skip_alternative();
  }
  else {
    if (if_else_stack.pop())
      skip_alternative();
    else
      begin_alternative();
  }
}

static int while_depth = 0;
static int while_break_flag = 0;

void while_request()
{
  macro mac;
  int escaped = 0;
  int level = 0;
  mac.append(new token_node(tok));
  for (;;) {
    node *n = 0;		// pacify compiler
    int c = input_stack::get(&n);
    if (c == EOF)
      break;
    if (c == 0) {
      escaped = 0;
      mac.append(n);
    }
    else if (escaped) {
      if (c == '{')
	level += 1;
      else if (c == '}')
	level -= 1;
      escaped = 0;
      mac.append(c);
    }
    else {
      if (c == ESCAPE_LEFT_BRACE)
	level += 1;
      else if (c == ESCAPE_RIGHT_BRACE)
	level -= 1;
      else if (c == escape_char)
	escaped = 1;
      mac.append(c);
      if (c == '\n' && level <= 0)
	break;
    }
  }
  if (level != 0)
    error("unbalanced brace escape sequences");
  else {
    while_depth++;
    input_stack::add_boundary();
    for (;;) {
      input_stack::push(new string_iterator(mac, "while loop"));
      tok.next();
      if (!do_if_request()) {
	while (input_stack::get(0) != EOF)
	  ;
	break;
      }
      process_input_stack();
      if (while_break_flag || input_stack::is_return_boundary()) {
	while_break_flag = 0;
	break;
      }
    }
    input_stack::remove_boundary();
    while_depth--;
  }
  tok.next();
}

void while_break_request()
{
  if (!while_depth) {
    error("no while loop");
    skip_line();
  }
  else {
    while_break_flag = 1;
    while (input_stack::get(0) != EOF)
      ;
    tok.next();
  }
}

void while_continue_request()
{
  if (!while_depth) {
    error("no while loop");
    skip_line();
  }
  else {
    while (input_stack::get(0) != EOF)
      ;
    tok.next();
  }
}

void do_source(bool quietly)
{
  symbol nm = get_long_name(true /* required */);
  if (nm.is_null())
    skip_line();
  else {
    while (!tok.is_newline() && !tok.is_eof())
      tok.next();
    errno = 0;
    FILE *fp = include_search_path.open_file_cautious(nm.contents());
    if (fp)
      input_stack::push(new file_iterator(fp, nm.contents()));
    else
      // Suppress diagnostic only if we're operating quietly and it's an
      // expected problem.
      if (!(quietly && (ENOENT == errno)))
	error("can't open '%1': %2", nm.contents(), strerror(errno));
    tok.next();
  }
}

// .so

void source()
{
  do_source(false /* not quietly*/ );
}

// .soquiet: like .so, but silently ignore files that can't be opened
// due to their nonexistence

void source_quietly()
{
  do_source(true /* quietly */ );
}

// like .so but use popen()

void pipe_source()
{
  if (!unsafe_flag) {
    error("'pso' request is not allowed in safer mode");
    skip_line();
  }
  else {
#ifdef POPEN_MISSING
    error("pipes not available on this system");
    skip_line();
#else /* not POPEN_MISSING */
    if (tok.is_newline() || tok.is_eof())
      error("missing command");
    else {
      int c;
      while ((c = get_copy(0)) == ' ' || c == '\t')
	;
      int buf_size = 24;
      char *buf = new char[buf_size];
      int buf_used = 0;
      for (; c != '\n' && c != EOF; c = get_copy(0)) {
	const char *s = asciify(c);
	int slen = strlen(s);
	if (buf_used + slen + 1> buf_size) {
	  char *old_buf = buf;
	  int old_buf_size = buf_size;
	  buf_size *= 2;
	  buf = new char[buf_size];
	  memcpy(buf, old_buf, old_buf_size);
	  delete[] old_buf;
	}
	strcpy(buf + buf_used, s);
	buf_used += slen;
      }
      buf[buf_used] = '\0';
      errno = 0;
      FILE *fp = popen(buf, POPEN_RT);
      if (fp)
	input_stack::push(new file_iterator(fp, symbol(buf).contents(), 1));
      else
	error("can't open pipe to process '%1': %2", buf, strerror(errno));
      delete[] buf;
    }
    tok.next();
#endif /* not POPEN_MISSING */
  }
}

// .psbb
//
// Extract bounding box limits from PostScript file, and assign
// them to the following four gtroff registers:--
//
static int llx_reg_contents = 0;
static int lly_reg_contents = 0;
static int urx_reg_contents = 0;
static int ury_reg_contents = 0;

// Manifest constants to specify the status of bounding box range
// acquisition; (note that PSBB_RANGE_IS_BAD is also suitable for
// assignment as a default ordinate property value).
//
#define PSBB_RANGE_IS_BAD   0
#define PSBB_RANGE_IS_SET   1
#define PSBB_RANGE_AT_END   2

// Maximum input line length, for DSC conformance, and options to
// control how it will be enforced; caller should select either of
// DSC_LINE_MAX_IGNORED, to allow partial line collection spread
// across multiple calls, or DSC_LINE_MAX_ENFORCE, to truncate
// excess length lines at the DSC limit.
//
// Note that DSC_LINE_MAX_CHECKED is reserved for internal use by
// ps_locator::get_line(), and should not be specified in any call;
// also, handling of DSC_LINE_MAX_IGNORED, as a get_line() option,
// is currently unimplemented.
//
#define DSC_LINE_MAX          255
#define DSC_LINE_MAX_IGNORED   -1
#define DSC_LINE_MAX_ENFORCE    0
#define DSC_LINE_MAX_CHECKED    1

// Input characters to be considered as white space, when reading
// PostScript file comments.
//
cset white_space("\n\r \t");

// Class psbb_locator
//
// This locally declared and implemented class provides the methods
// to be used for retrieval of bounding box properties from a specified
// PostScript or PDF file.
//
class psbb_locator
{
  public:
    // Only the class constructor is exposed publicly; instantiation of
    // a class object will retrieve the requisite bounding box properties
    // from the specified file, and assign them to gtroff registers.
    //
    psbb_locator(const char *);

  private:
    FILE *fp;
    const char *filename;
    char buf[2 + DSC_LINE_MAX];
    int llx, lly, urx, ury;

    // CRLF handling hook, for get_line() function.
    //
    int lastc;

    // Private method functions facilitate implementation of the
    // class constructor; none are used in any other context.
    //
    int get_line(int);
    inline bool get_header_comment(void);
    inline const char *context_args(const char *);
    inline const char *context_args(const char *, const char *);
    inline const char *bounding_box_args(void);
    int parse_bounding_box(const char *);
    inline void assign_registers(void);
    inline int skip_to_trailer(void);
};

// psbb_locator class constructor.
//
psbb_locator::psbb_locator(const char *fname):
filename(fname), llx(0), lly(0), urx(0), ury(0), lastc(EOF)
{
  // PS files might contain non-printable characters, such as ^Z
  // and CRs not followed by an LF, so open them in binary mode.
  //
  fp = include_search_path.open_file_cautious(filename, 0, FOPEN_RB);
  if (fp) {
    // After successfully opening the file, acquire the first
    // line, whence we may determine the file format...
    //
    if (get_line(DSC_LINE_MAX_ENFORCE) == 0)
      //
      // ...except in the case of an empty file, which we are
      // unable to process further.
      //
      error("'%1' is empty", filename);

# if 0
    else if (context_args("%PDF-")) {
      // TODO: PDF files specify a /MediaBox, as the equivalent
      // of %%BoundingBox; we must implement a handler for this.
    }
# endif

    else if (context_args("%!PS-Adobe-")) {
      //
      // PostScript files -- strictly, we expect EPS -- should
      // specify a %%BoundingBox comment; locate it, initially
      // expecting to find it in the comments header...
      //
      const char *context = NULL;
      while ((context == NULL) && get_header_comment()) {
	if ((context = bounding_box_args()) != NULL) {

	  // When the "%%BoundingBox" comment is found, it may simply
	  // specify the bounding box property values, or it may defer
	  // assignment to a similar trailer comment...
	  //
	  int status = parse_bounding_box(context);
	  if (status == PSBB_RANGE_AT_END) {
	    //
	    // ...in which case we must locate the trailer, and search
	    // for the appropriate specification within it.
	    //
	    if (skip_to_trailer() > 0) {
	      while ((context = bounding_box_args()) == NULL
		     && get_line(DSC_LINE_MAX_ENFORCE) > 0)
		;
	      if (context != NULL) {
		//
		// When we find a bounding box specification here...
		//
		if ((status = parse_bounding_box(context)) == PSBB_RANGE_AT_END)
		  //
		  // ...we must ensure it is not a further attempt to defer
		  // assignment to a trailer, (which we are already parsing).
		  //
		  error("'(atend)' is not allowed in trailer of '%1'",
			filename);
	      }
	    }
	    else
	      // The trailer could not be found, so there is no context in
	      // which a trailing %%BoundingBox comment might be located.
	      //
	      context = NULL;
	  }
	  if (status == PSBB_RANGE_IS_BAD) {
	    //
	    // This arises when we found a %%BoundingBox comment, but
	    // we were unable to extract a valid set of range values from
	    // it; all we can do is diagnose this.
	    //
	    error("the arguments to the %%%%BoundingBox comment in '%1' are bad",
		  filename);
	  }
	}
      }
      if (context == NULL)
	//
	// Conversely, this arises when no value specifying %%BoundingBox
	// comment has been found, in any appropriate location...
	//
	error("%%%%BoundingBox comment not found in '%1'", filename);
    }
    else
      // ...while this indicates that there was no appropriate file format
      // identifier, on the first line of the input file.
      //
      error("'%1' does not conform to the Document Structuring Conventions",
	    filename);

    // Regardless of success or failure of bounding box property acquisition,
    // we did successfully open an input file, so we must now close it...
    //
    fclose(fp);
  }
  else
    // ...but in this case, we did not successfully open any input file.
    //
    error("can't open '%1': %2", filename, strerror(errno));

  // Irrespective of whether or not we were able to successfully acquire the
  // bounding box properties, we ALWAYS update the associated gtroff registers.
  //
  assign_registers();
}

// psbb_locator::parse_bounding_box()
//
// Parse the argument to a %%BoundingBox comment, returning:
//   PSBB_RANGE_IS_SET if it contains four numbers,
//   PSBB_RANGE_AT_END if it contains "(atend)", or
//   PSBB_RANGE_IS_BAD otherwise.
//
int psbb_locator::parse_bounding_box(const char *context)
{
  // The Document Structuring Conventions say that the numbers
  // should be integers.
  //
  int status = PSBB_RANGE_IS_SET;
  if (sscanf(context, "%d %d %d %d", &llx, &lly, &urx, &ury) != 4) {
    //
    // Unfortunately some broken applications get this wrong;
    // try to parse them as doubles instead...
    //
    double x1, x2, x3, x4;
    if (sscanf(context, "%lf %lf %lf %lf", &x1, &x2, &x3, &x4) == 4) {
      llx = (int)x1;
      lly = (int)x2;
      urx = (int)x3;
      ury = (int)x4;
    }
    else {
      // ...but if we can't parse four numbers, skip over any
      // initial white space...
      //
      while (*context == '\x20' || *context == '\t')
	context++;

      // ...before checking for "(atend)", and setting the
      // appropriate exit status accordingly.
      //
      status = (context_args("(atend)", context) == NULL)
		 ? llx = lly = urx = ury = PSBB_RANGE_IS_BAD
		 : PSBB_RANGE_AT_END;
    }
  }
  return status;
}

// ps_locator::get_line()
//
// Collect an input record from a PostScript or PDF file.
//
// Inputs:
//   buf       pointer to caller's input buffer.
//   fp        FILE stream pointer, whence input is read.
//   filename  name of input file, (for diagnostic use only).
//   dscopt    DSC_LINE_MAX_ENFORCE or DSC_LINE_MAX_IGNORED.
//
// Returns the number of input characters stored into caller's
// buffer, or zero at end of input stream.
//
// FIXME: Currently, get_line() always scans an entire line of
// input, but returns only as much as will fit in caller's buffer;
// the return value is always a positive integer, or zero, with no
// way of indicating to caller, that there was more data than the
// buffer could accommodate.  A future enhancement could mitigate
// this, returning a negative value in the event of truncation, or
// even allowing for piecewise retrieval of excessively long lines
// in successive reads; (this may be necessary to properly support
// DSC_LINE_MAX_IGNORED, which is currently unimplemented).
//
int psbb_locator::get_line(int dscopt)
{
  int c, count = 0;
  do {
    // Collect input characters into caller's buffer, until we
    // encounter a line terminator, or end of file...
    //
    while (((c = getc(fp)) != '\n') && (c != '\r') && (c != EOF)) {
      if ((((lastc = c) < 0x1b) && !white_space(c)) || (c == 0x7f))
	//
	// ...rejecting any which may be designated as invalid.
	//
	error("invalid input character code %1 in '%2'", int(c), filename);

      // On reading a valid input character, and when there is
      // room in caller's buffer...
      //
      else if (count < DSC_LINE_MAX)
	//
	// ...store it.
	//
	buf[count++] = c;

      // We have a valid input character, but it will not fit
      // into caller's buffer; if enforcing DSC conformity...
      //
      else if (dscopt == DSC_LINE_MAX_ENFORCE) {
	//
	// ...diagnose and truncate.
	//
	dscopt = DSC_LINE_MAX_CHECKED;
	error("PostScript file '%1' is non-conforming "
	      "because length of line exceeds 255", filename);
      }
    }
    // Reading LF may be a special case: when it immediately
    // follows a CR which terminated the preceding input line,
    // we deem it to complete a CRLF terminator for the already
    // collected preceding line; discard it, and restart input
    // collection for the current line.
    //
  } while ((lastc == '\r') && ((lastc = c) == '\n'));

  // For each collected input line, record its actual terminator,
  // substitute our preferred LF terminator...
  //
  if (((lastc = c) != EOF) || (count > 0))
    buf[count++] = '\n';

  // ...and append the required C-string (NUL) terminator, before
  // returning the actual count of input characters stored.
  //
  buf[count] = '\0';
  return count;
}

// psbb_locator::context_args()
//
// Inputs:
//   tag   literal text to be matched at start of input line
//
// Returns a pointer to the trailing substring of the current
// input line, following an initial substring matching the "tag"
// argument, or NULL if "tag" is not matched.
//
inline const char *psbb_locator::context_args(const char *tag)
{
  return context_args(tag, buf);
}

// psbb_locator::context_args()
//
// Overloaded variant of the preceding function, operating on
// an alternative input buffer, (which may represent a terminal
// substring of the psbb_locator's primary input line buffer).
//
// Inputs:
//   tag   literal text to be matched at start of buffer
//   p     pointer to text to be checked for "tag" match
//
// Returns a pointer to the trailing substring of the specified
// text buffer, following an initial substring matching the "tag"
// argument, or NULL if "tag" is not matched.
//
inline const char *psbb_locator::context_args(const char *tag, const char *p)
{
  size_t len = strlen(tag);
  return (strncmp(tag, p, len) == 0) ? p + len : NULL;
}

// psbb_locator::bounding_box_args()
//
// Returns a pointer to the arguments string, within the current
// input line, when this represents a PostScript "%%BoundingBox:"
// comment, or NULL otherwise.
//
inline const char *psbb_locator::bounding_box_args(void)
{
  return context_args("%%BoundingBox:");
}

// psbb_locator::assign_registers()
//
// Copies the bounding box properties established within the
// class object, to the associated gtroff registers.
//
inline void psbb_locator::assign_registers(void)
{
  llx_reg_contents = llx;
  lly_reg_contents = lly;
  urx_reg_contents = urx;
  ury_reg_contents = ury;
}

// psbb_locator::get_header_comment()
//
// Fetch a line of PostScript input; return true if it complies with
// the formatting requirements for header comments, and it is not an
// "%%EndComments" line; otherwise return false.
//
inline bool psbb_locator::get_header_comment(void)
{
  return
    // The first necessary requirement, for returning true,
    // is that the input line is not empty, (i.e. not EOF).
    //
    get_line(DSC_LINE_MAX_ENFORCE) != 0

    // In header comments, '%X' ('X' any printable character
    // except whitespace) is also acceptable.
    //
    && (buf[0] == '%') && !white_space(buf[1])

    // Finally, the input line must not say "%%EndComments".
    //
    && context_args("%%EndComments") == NULL;
}

// psbb_locator::skip_to_trailer()
//
// Reposition the PostScript input stream, such that the next get_line()
// will retrieve the first line, if any, following a "%%Trailer" comment;
// returns a positive integer value if the "%%Trailer" comment is found,
// or zero if it is not.
//
inline int psbb_locator::skip_to_trailer(void)
{
  // Begin by considering a chunk of the input file starting 512 bytes
  // before its end, and search it for a "%%Trailer" comment; if none is
  // found, incrementally double the chunk size while it remains within
  // a 32768L byte range, and search again...
  //
  for (ssize_t offset = 512L; offset > 0L; offset <<= 1) {
    int status, failed;
    if ((offset > 32768L) || ((failed = fseek(fp, -offset, SEEK_END)) != 0))
      //
      // ...ultimately resetting the offset to zero, and simply seeking
      // to the start of the file, to terminate the cycle and do a "last
      // ditch" search of the entire file, if any backward seek fails, or
      // if we reach the arbitrary 32768L byte range limit.
      //
      failed = fseek(fp, offset = 0L, SEEK_SET);

    // Following each successful seek...
    //
    if (!failed) {
      //
      // ...perform a search by reading lines from the input stream...
      //
      do { status = get_line(DSC_LINE_MAX_ENFORCE);
	   //
	   // ...until we either exhaust the available stream data, or
	   // we have located a "%%Trailer" comment line.
	   //
	 } while ((status != 0) && (context_args("%%Trailer") == NULL));
      if (status > 0)
	//
	// We found the "%%Trailer" comment, so we may immediately
	// return, with the stream positioned appropriately...
	//
	return status;
    }
  }
  // ...otherwise, we report that no "%%Trailer" comment was found.
  //
  return 0;
}

// ps_bbox_request()
//
// Handle the .psbb request.
//
void ps_bbox_request()
{
  // Parse input line, to extract file name.
  //
  symbol nm = get_long_name(true /* required */);
  if (nm.is_null())
    //
    // No file name specified: ignore the entire request.
    //
    skip_line();
  else {
    // File name acquired: swallow the rest of the line.
    //
    while (!tok.is_newline() && !tok.is_eof())
      tok.next();
    errno = 0;

    // Update {llx,lly,urx,ury}_reg_contents:
    // declaring this class instance achieves this, as an
    // intentional side effect of object construction.
    //
    psbb_locator do_ps_file(nm.contents());

    // All done for .psbb; move on, to continue
    // input stream processing.
    //
    tok.next();
  }
}

const char *asciify(int c)
{
  static char buf[3];
  buf[0] = escape_char == '\0' ? '\\' : escape_char;
  buf[1] = buf[2] = '\0';
  switch (c) {
  case ESCAPE_QUESTION:
    buf[1] = '?';
    break;
  case ESCAPE_AMPERSAND:
    buf[1] = '&';
    break;
  case ESCAPE_RIGHT_PARENTHESIS:
    buf[1] = ')';
    break;
  case ESCAPE_UNDERSCORE:
    buf[1] = '_';
    break;
  case ESCAPE_BAR:
    buf[1] = '|';
    break;
  case ESCAPE_CIRCUMFLEX:
    buf[1] = '^';
    break;
  case ESCAPE_LEFT_BRACE:
    buf[1] = '{';
    break;
  case ESCAPE_RIGHT_BRACE:
    buf[1] = '}';
    break;
  case ESCAPE_LEFT_QUOTE:
    buf[1] = '`';
    break;
  case ESCAPE_RIGHT_QUOTE:
    buf[1] = '\'';
    break;
  case ESCAPE_HYPHEN:
    buf[1] = '-';
    break;
  case ESCAPE_BANG:
    buf[1] = '!';
    break;
  case ESCAPE_c:
    buf[1] = 'c';
    break;
  case ESCAPE_e:
    buf[1] = 'e';
    break;
  case ESCAPE_E:
    buf[1] = 'E';
    break;
  case ESCAPE_PERCENT:
    buf[1] = '%';
    break;
  case ESCAPE_SPACE:
    buf[1] = ' ';
    break;
  case ESCAPE_TILDE:
    buf[1] = '~';
    break;
  case ESCAPE_COLON:
    buf[1] = ':';
    break;
  case PUSH_GROFF_MODE:
  case PUSH_COMP_MODE:
  case POP_GROFFCOMP_MODE:
    buf[0] = '\0';
    break;
  default:
    if (is_invalid_input_char(c))
      buf[0] = '\0';
    else
      buf[0] = c;
    break;
  }
  return buf;
}

const char *input_char_description(int c)
{
  switch (c) {
  case '\n':
    return "a newline character";
  case '\b':
    return "a backspace character";
  case '\001':
    return "a leader character";
  case '\t':
    return "a tab character";
  case ' ':
    return "a space character";
  case '\0':
    return "a node";
  }
  size_t bufsz = sizeof "magic character code " + INT_DIGITS + 1;
  // repeat expression; no VLAs in ISO C++
  static char buf[sizeof "magic character code " + INT_DIGITS + 1];
  (void) memset(buf, 0, bufsz);
  if (is_invalid_input_char(c)) {
    const char *s = asciify(c);
    if (*s) {
      buf[0] = '\'';
      strcpy(buf + 1, s);
      strcat(buf, "'");
      return buf;
    }
    sprintf(buf, "magic character code %d", c);
    return buf;
  }
  if (csprint(c)) {
    buf[0] = '\'';
    buf[1] = c;
    buf[2] = '\'';
    return buf;
  }
  sprintf(buf, "character code %d", c);
  return buf;
}

void tag()
{
  if (!tok.is_newline() && !tok.is_eof()) {
    string s;
    int c;
    for (;;) {
      c = get_copy(0);
      if (c == '"') {
	c = get_copy(0);
	break;
      }
      if (c != ' ' && c != '\t')
	break;
    }
    s = "x X ";
    for (; c != '\n' && c != EOF; c = get_copy(0))
      s += (char)c;
    s += '\n';
    curenv->add_node(new tag_node(s, 0));
  }
  tok.next();
}

void taga()
{
  if (!tok.is_newline() && !tok.is_eof()) {
    string s;
    int c;
    for (;;) {
      c = get_copy(0);
      if (c == '"') {
	c = get_copy(0);
	break;
      }
      if (c != ' ' && c != '\t')
	break;
    }
    s = "x X ";
    for (; c != '\n' && c != EOF; c = get_copy(0))
      s += (char)c;
    s += '\n';
    curenv->add_node(new tag_node(s, 1));
  }
  tok.next();
}

// .tm, .tm1, and .tmc

void do_terminal(int newline, int string_like)
{
  if (!tok.is_newline() && !tok.is_eof()) {
    int c;
    for (;;) {
      c = get_copy(0);
      if (string_like && c == '"') {
	c = get_copy(0);
	break;
      }
      if (c != ' ' && c != '\t')
	break;
    }
    for (; c != '\n' && c != EOF; c = get_copy(0))
      fputs(asciify(c), stderr);
  }
  if (newline)
    fputc('\n', stderr);
  fflush(stderr);
  tok.next();
}

void terminal()
{
  do_terminal(1, 0);
}

void terminal1()
{
  do_terminal(1, 1);
}

void terminal_continue()
{
  do_terminal(0, 1);
}

dictionary stream_dictionary(20);

void do_open(int append)
{
  symbol stream = get_name(true /* required */);
  if (!stream.is_null()) {
    symbol filename = get_long_name(true /* required */);
    if (!filename.is_null()) {
      errno = 0;
      FILE *fp = fopen(filename.contents(), append ? "a" : "w");
      if (!fp) {
	error("can't open '%1' for %2: %3",
	      filename.contents(),
	      append ? "appending" : "writing",
	      strerror(errno));
	fp = (FILE *)stream_dictionary.remove(stream);
      }
      else
	fp = (FILE *)stream_dictionary.lookup(stream, fp);
      if (fp)
	fclose(fp);
    }
  }
  skip_line();
}

void open_request()
{
  if (!unsafe_flag) {
    error("'open' request is not allowed in safer mode");
    skip_line();
  }
  else
    do_open(0);
}

void opena_request()
{
  if (!unsafe_flag) {
    error("'opena' request is not allowed in safer mode");
    skip_line();
  }
  else
    do_open(1);
}

void close_request()
{
  symbol stream = get_name(true /* required */);
  if (!stream.is_null()) {
    FILE *fp = (FILE *)stream_dictionary.remove(stream);
    if (!fp)
      error("no stream named '%1'", stream.contents());
    else
      fclose(fp);
  }
  skip_line();
}

// .write and .writec

void do_write_request(int newline)
{
  symbol stream = get_name(true /* required */);
  if (stream.is_null()) {
    skip_line();
    return;
  }
  FILE *fp = (FILE *)stream_dictionary.lookup(stream);
  if (!fp) {
    error("no stream named '%1'", stream.contents());
    skip_line();
    return;
  }
  int c;
  while ((c = get_copy(0)) == ' ')
    ;
  if (c == '"')
    c = get_copy(0);
  for (; c != '\n' && c != EOF; c = get_copy(0))
    fputs(asciify(c), fp);
  if (newline)
    fputc('\n', fp);
  fflush(fp);
  tok.next();
}

void write_request()
{
  do_write_request(1);
}

void write_request_continue()
{
  do_write_request(0);
}

void write_macro_request()
{
  symbol stream = get_name(true /* required */);
  if (stream.is_null()) {
    skip_line();
    return;
  }
  FILE *fp = (FILE *)stream_dictionary.lookup(stream);
  if (!fp) {
    error("no stream named '%1'", stream.contents());
    skip_line();
    return;
  }
  symbol s = get_name(true /* required */);
  if (s.is_null()) {
    skip_line();
    return;
  }
  request_or_macro *p = lookup_request(s);
  macro *m = p->to_macro();
  if (!m)
    error("cannot write request");
  else {
    string_iterator iter(*m);
    for (;;) {
      int c = iter.get(0);
      if (c == EOF)
	break;
      fputs(asciify(c), fp);
    }
    fflush(fp);
  }
  skip_line();
}

void warnscale_request()
{
  if (has_arg()) {
    char c = tok.ch();
    if (c == 'u')
      warn_scale = 1.0;
    else if (c == 'i')
      warn_scale = (double)units_per_inch;
    else if (c == 'c')
      warn_scale = (double)units_per_inch / 2.54;
    else if (c == 'p')
      warn_scale = (double)units_per_inch / 72.0;
    else if (c == 'P')
      warn_scale = (double)units_per_inch / 6.0;
    else {
      warning(WARN_SCALE,
	      "scaling unit '%1' invalid; using 'i' instead", c);
      c = 'i';
    }
    warn_scaling_indicator = c;
  }
  skip_line();
}

void spreadwarn_request()
{
  hunits n;
  if (has_arg() && get_hunits(&n, 'm')) {
    if (n < 0)
      n = 0;
    hunits em = curenv->get_size();
    spread_limit = (double)n.to_units()
		   / (em.is_zero() ? hresolution : em.to_units());
  }
  else
    spread_limit = -spread_limit - 1;	// no arg toggles on/off without
					// changing value; we mirror at
					// -0.5 to make zero a valid value
  skip_line();
}

static void init_charset_table()
{
  char buf[16];
  strcpy(buf, "char");
  for (int i = 0; i < 256; i++) {
    strcpy(buf + 4, i_to_a(i));
    charset_table[i] = get_charinfo(symbol(buf));
    charset_table[i]->set_ascii_code(i);
    if (csalpha(i))
      charset_table[i]->set_hyphenation_code(cmlower(i));
  }
  charset_table['.']->set_flags(charinfo::ENDS_SENTENCE);
  charset_table['?']->set_flags(charinfo::ENDS_SENTENCE);
  charset_table['!']->set_flags(charinfo::ENDS_SENTENCE);
  charset_table['-']->set_flags(charinfo::BREAK_AFTER);
  charset_table['"']->set_flags(charinfo::TRANSPARENT);
  charset_table['\'']->set_flags(charinfo::TRANSPARENT);
  charset_table[')']->set_flags(charinfo::TRANSPARENT);
  charset_table[']']->set_flags(charinfo::TRANSPARENT);
  charset_table['*']->set_flags(charinfo::TRANSPARENT);
  get_charinfo(symbol("dg"))->set_flags(charinfo::TRANSPARENT);
  get_charinfo(symbol("dd"))->set_flags(charinfo::TRANSPARENT);
  get_charinfo(symbol("rq"))->set_flags(charinfo::TRANSPARENT);
  get_charinfo(symbol("cq"))->set_flags(charinfo::TRANSPARENT);
  get_charinfo(symbol("em"))->set_flags(charinfo::BREAK_AFTER);
  get_charinfo(symbol("hy"))->set_flags(charinfo::BREAK_AFTER);
  get_charinfo(symbol("ul"))->set_flags(charinfo::OVERLAPS_HORIZONTALLY);
  get_charinfo(symbol("rn"))->set_flags(charinfo::OVERLAPS_HORIZONTALLY);
  get_charinfo(symbol("radicalex"))->set_flags(charinfo::OVERLAPS_HORIZONTALLY);
  get_charinfo(symbol("sqrtex"))->set_flags(charinfo::OVERLAPS_HORIZONTALLY);
  get_charinfo(symbol("ru"))->set_flags(charinfo::OVERLAPS_HORIZONTALLY);
  get_charinfo(symbol("br"))->set_flags(charinfo::OVERLAPS_VERTICALLY);
  page_character = charset_table['%'];
}

static void init_hpf_code_table()
{
  for (int i = 0; i < 256; i++)
    hpf_code_table[i] = cmlower(i);
}

static void do_translate(int translate_transparent, int translate_input)
{
  tok.skip();
  while (!tok.is_newline() && !tok.is_eof()) {
    if (tok.is_space()) {
      // This is a really bizarre troff feature.
      tok.next();
      translate_space_to_dummy = tok.is_dummy();
      if (tok.is_newline() || tok.is_eof())
	break;
      error("cannot translate space character; ignoring");
      tok.next();
      continue;
    }
    charinfo *ci1 = tok.get_char(true /* required */);
    if (ci1 == 0)
      break;
    tok.next();
    if (tok.is_newline() || tok.is_eof()) {
      ci1->set_special_translation(charinfo::TRANSLATE_SPACE,
				   translate_transparent);
      break;
    }
    if (tok.is_space())
      ci1->set_special_translation(charinfo::TRANSLATE_SPACE,
				   translate_transparent);
    else if (tok.is_stretchable_space())
      ci1->set_special_translation(charinfo::TRANSLATE_STRETCHABLE_SPACE,
				   translate_transparent);
    else if (tok.is_dummy())
      ci1->set_special_translation(charinfo::TRANSLATE_DUMMY,
				   translate_transparent);
    else if (tok.is_hyphen_indicator())
      ci1->set_special_translation(charinfo::TRANSLATE_HYPHEN_INDICATOR,
				   translate_transparent);
    else {
      charinfo *ci2 = tok.get_char(true /* required */);
      if (ci2 == 0)
	break;
      if (ci1 == ci2)
	ci1->set_translation(0, translate_transparent, translate_input);
      else
	ci1->set_translation(ci2, translate_transparent, translate_input);
    }
    tok.next();
  }
  skip_line();
}

void translate()
{
  do_translate(1, 0);
}

void translate_no_transparent()
{
  do_translate(0, 0);
}

void translate_input()
{
  do_translate(1, 1);
}

void char_flags()
{
  int flags;
  if (get_integer(&flags))
    while (has_arg()) {
      charinfo *ci = tok.get_char(true /* required */);
      if (ci) {
	charinfo *tem = ci->get_translation();
	if (tem)
	  ci = tem;
	ci->set_flags(flags);
      }
      tok.next();
    }
  skip_line();
}

void hyphenation_code()
{
  tok.skip();
  while (!tok.is_newline() && !tok.is_eof()) {
    charinfo *ci = tok.get_char(true /* required */);
    if (ci == 0)
      break;
    tok.next();
    tok.skip();
    unsigned char c = tok.ch();
    if (c == 0) {
      error("hyphenation code must be ordinary character");
      break;
    }
    if (csdigit(c)) {
      error("hyphenation code cannot be digit");
      break;
    }
    ci->set_hyphenation_code(c);
    if (ci->get_translation()
	&& ci->get_translation()->get_translation_input())
      ci->get_translation()->set_hyphenation_code(c);
    tok.next();
    tok.skip();
  }
  skip_line();
}

void hyphenation_patterns_file_code()
{
  tok.skip();
  while (!tok.is_newline() && !tok.is_eof()) {
    int n1, n2;
    if (get_integer(&n1) && (0 <= n1 && n1 <= 255)) {
      if (!has_arg()) {
	error("missing output hyphenation code");
	break;
      }
      if (get_integer(&n2) && (0 <= n2 && n2 <= 255)) {
	hpf_code_table[n1] = n2;
	tok.skip();
      }
      else {
	error("output hyphenation code must be integer in the range 0..255");
	break;
      }
    }
    else {
      error("input hyphenation code must be integer in the range 0..255");
      break;
    }
  }
  skip_line();
}

dictionary char_class_dictionary(501);

void define_class()
{
  tok.skip();
  symbol nm = get_name(true /* required */);
  if (nm.is_null()) {
    skip_line();
    return;
  }
  charinfo *ci = get_charinfo(nm);
  charinfo *child1 = 0, *child2 = 0;
  while (!tok.is_newline() && !tok.is_eof()) {
    tok.skip();
    if (child1 != 0 && tok.ch() == '-') {
      tok.next();
      child2 = tok.get_char(true /* required */);
      if (!child2) {
	warning(WARN_MISSING,
		"missing end of character range in class '%1'",
		nm.contents());
	skip_line();
	return;
      }
      if (child1->is_class() || child2->is_class()) {
	warning(WARN_SYNTAX,
		"a nested character class is not allowed in a range"
		" definition");
	skip_line();
	return;
      }
      int u1 = child1->get_unicode_code();
      int u2 = child2->get_unicode_code();
      if (u1 < 0) {
	warning(WARN_SYNTAX,
		"invalid start value in character range");
	skip_line();
	return;
      }
      if (u2 < 0) {
	warning(WARN_SYNTAX,
		"invalid end value in character range");
	skip_line();
	return;
      }
      ci->add_to_class(u1, u2);
      child1 = child2 = 0;
    }
    else if (child1 != 0) {
      if (child1->is_class()) {
	if (ci == child1) {
	  warning(WARN_SYNTAX, "invalid cyclic class nesting");
	  skip_line();
	  return;
	}
	ci->add_to_class(child1);
      }
      else {
	int u1 = child1->get_unicode_code();
	if (u1 < 0) {
	  warning(WARN_SYNTAX,
		  "invalid character value in class '%1'",
		  nm.contents());
	  skip_line();
	  return;
	}
	ci->add_to_class(u1);
      }
      child1 = 0;
    }
    child1 = tok.get_char(true /* required */);
    tok.next();
    if (!child1) {
      if (!tok.is_newline())
	skip_line();
      break;
    }
  }
  if (child1 != 0) {
    if (child1->is_class()) {
      if (ci == child1) {
	warning(WARN_SYNTAX, "invalid cyclic class nesting");
	skip_line();
	return;
      }
      ci->add_to_class(child1);
    }
    else {
      int u1 = child1->get_unicode_code();
      if (u1 < 0) {
	warning(WARN_SYNTAX,
		"invalid character value in class '%1'",
		nm.contents());
	skip_line();
	return;
      }
      ci->add_to_class(u1);
    }
    child1 = 0;
  }
  if (!ci->is_class()) {
    warning(WARN_SYNTAX,
	    "empty class definition for '%1'",
	    nm.contents());
    skip_line();
    return;
  }
  (void)char_class_dictionary.lookup(nm, ci);
  skip_line();
}

charinfo *token::get_char(bool required)
{
  if (type == TOKEN_CHAR)
    return charset_table[c];
  if (type == TOKEN_SPECIAL)
    return get_charinfo(nm);
  if (type == TOKEN_NUMBERED_CHAR)
    return get_charinfo_by_number(val);
  if (type == TOKEN_ESCAPE) {
    if (escape_char != 0)
      return charset_table[escape_char];
    else {
      // XXX: Is this possible?  token::add_to_zero_width_node_list()
      // and token::process() don't add this token type if the escape
      // character is null.  If not, this should be an assert().  Also
      // see escape_off().
      error("escaped 'e' used while escape sequences disabled");
      return 0;
    }
  }
  if (required) {
    if (type == TOKEN_EOF || type == TOKEN_NEWLINE)
      warning(WARN_MISSING, "missing ordinary or special character");
    else
      error("expected ordinary or special character, got %1",
	    description());
  }
  return 0;
}

charinfo *get_optional_char()
{
  while (tok.is_space())
    tok.next();
  charinfo *ci = tok.get_char();
  if (!ci)
    check_missing_character();
  else
    tok.next();
  return ci;
}

void check_missing_character()
{
  if (!tok.is_newline() && !tok.is_eof() && !tok.is_right_brace()
      && !tok.is_tab())
    error("expected ordinary or special character, got %1; treated as"
	  " missing", tok.description());
}

// this is for \Z

int token::add_to_zero_width_node_list(node **pp)
{
  hunits w;
  int s;
  node *n = 0;
  switch (type) {
  case TOKEN_CHAR:
    *pp = (*pp)->add_char(charset_table[c], curenv, &w, &s);
    break;
  case TOKEN_DUMMY:
    n = new dummy_node;
    break;
  case TOKEN_ESCAPE:
    if (escape_char != 0)
      *pp = (*pp)->add_char(charset_table[escape_char], curenv, &w, &s);
    break;
  case TOKEN_HYPHEN_INDICATOR:
    *pp = (*pp)->add_discretionary_hyphen();
    break;
  case TOKEN_ITALIC_CORRECTION:
    *pp = (*pp)->add_italic_correction(&w);
    break;
  case TOKEN_LEFT_BRACE:
    break;
  case TOKEN_MARK_INPUT:
    set_number_reg(nm, curenv->get_input_line_position().to_units());
    break;
  case TOKEN_NODE:
  case TOKEN_HORIZONTAL_SPACE:
    n = nd;
    nd = 0;
    break;
  case TOKEN_NUMBERED_CHAR:
    *pp = (*pp)->add_char(get_charinfo_by_number(val), curenv, &w, &s);
    break;
  case TOKEN_RIGHT_BRACE:
    break;
  case TOKEN_SPACE:
    n = new hmotion_node(curenv->get_space_width(),
			 curenv->get_fill_color());
    break;
  case TOKEN_SPECIAL:
    *pp = (*pp)->add_char(get_charinfo(nm), curenv, &w, &s);
    break;
  case TOKEN_STRETCHABLE_SPACE:
    n = new unbreakable_space_node(curenv->get_space_width(),
				   curenv->get_fill_color());
    break;
  case TOKEN_UNSTRETCHABLE_SPACE:
    n = new space_char_hmotion_node(curenv->get_space_width(),
				    curenv->get_fill_color());
    break;
  case TOKEN_TRANSPARENT_DUMMY:
    n = new transparent_dummy_node;
    break;
  case TOKEN_ZERO_WIDTH_BREAK:
    n = new space_node(H0, curenv->get_fill_color());
    n->freeze_space();
    n->is_escape_colon();
    break;
  default:
    return 0;
  }
  if (n) {
    n->next = *pp;
    *pp = n;
  }
  return 1;
}

void token::process()
{
  if (possibly_handle_first_page_transition())
    return;
  switch (type) {
  case TOKEN_BACKSPACE:
    curenv->add_node(new hmotion_node(-curenv->get_space_width(),
				      curenv->get_fill_color()));
    break;
  case TOKEN_CHAR:
    curenv->add_char(charset_table[c]);
    break;
  case TOKEN_DUMMY:
    curenv->add_node(new dummy_node);
    break;
  case TOKEN_EMPTY:
    assert(0);
    break;
  case TOKEN_EOF:
    assert(0);
    break;
  case TOKEN_ESCAPE:
    if (escape_char != 0)
      curenv->add_char(charset_table[escape_char]);
    break;
  case TOKEN_BEGIN_TRAP:
  case TOKEN_END_TRAP:
  case TOKEN_PAGE_EJECTOR:
    // these are all handled in process_input_stack()
    break;
  case TOKEN_HYPHEN_INDICATOR:
    curenv->add_hyphen_indicator();
    break;
  case TOKEN_INTERRUPT:
    curenv->interrupt();
    break;
  case TOKEN_ITALIC_CORRECTION:
    curenv->add_italic_correction();
    break;
  case TOKEN_LEADER:
    curenv->handle_tab(1);
    break;
  case TOKEN_LEFT_BRACE:
    break;
  case TOKEN_MARK_INPUT:
    set_number_reg(nm, curenv->get_input_line_position().to_units());
    break;
  case TOKEN_NEWLINE:
    curenv->newline();
    break;
  case TOKEN_NODE:
  case TOKEN_HORIZONTAL_SPACE:
    curenv->add_node(nd);
    nd = 0;
    break;
  case TOKEN_NUMBERED_CHAR:
    curenv->add_char(get_charinfo_by_number(val));
    break;
  case TOKEN_REQUEST:
    // handled in process_input_stack()
    break;
  case TOKEN_RIGHT_BRACE:
    break;
  case TOKEN_SPACE:
    curenv->space();
    break;
  case TOKEN_SPECIAL:
    curenv->add_char(get_charinfo(nm));
    break;
  case TOKEN_SPREAD:
    curenv->spread();
    break;
  case TOKEN_STRETCHABLE_SPACE:
    curenv->add_node(new unbreakable_space_node(curenv->get_space_width(),
						curenv->get_fill_color()));
    break;
  case TOKEN_UNSTRETCHABLE_SPACE:
    curenv->add_node(new space_char_hmotion_node(curenv->get_space_width(),
						 curenv->get_fill_color()));
    break;
  case TOKEN_TAB:
    curenv->handle_tab(0);
    break;
  case TOKEN_TRANSPARENT:
    break;
  case TOKEN_TRANSPARENT_DUMMY:
    curenv->add_node(new transparent_dummy_node);
    break;
  case TOKEN_ZERO_WIDTH_BREAK:
    {
      node *tmp = new space_node(H0, curenv->get_fill_color());
      tmp->freeze_space();
      tmp->is_escape_colon();
      curenv->add_node(tmp);
      break;
    }
  default:
    assert(0);
  }
}

class nargs_reg : public reg {
public:
  const char *get_string();
};

const char *nargs_reg::get_string()
{
  return i_to_a(input_stack::nargs());
}

class lineno_reg : public reg {
public:
  const char *get_string();
};

const char *lineno_reg::get_string()
{
  int line;
  const char *file;
  if (!input_stack::get_location(0, &file, &line))
    line = 0;
  return i_to_a(line);
}

class writable_lineno_reg : public general_reg {
public:
  writable_lineno_reg();
  void set_value(units);
  bool get_value(units *);
};

writable_lineno_reg::writable_lineno_reg()
{
}

bool writable_lineno_reg::get_value(units *res)
{
  int line;
  const char *file;
  if (!input_stack::get_location(0, &file, &line))
    return false;
  *res = line;
  return true;
}

void writable_lineno_reg::set_value(units n)
{
  input_stack::set_location(0, n);
}

class filename_reg : public reg {
public:
  const char *get_string();
};

const char *filename_reg::get_string()
{
  int line;
  const char *file;
  if (input_stack::get_location(0, &file, &line))
    return file;
  else
    return 0;
}

class break_flag_reg : public reg {
public:
  const char *get_string();
};

const char *break_flag_reg::get_string()
{
  return i_to_a(input_stack::get_break_flag());
}

class readonly_text_register : public reg {
  const char *s;
public:
  readonly_text_register(const char *);
  const char *get_string();
};

readonly_text_register::readonly_text_register(const char *p) : s(p)
{
}

const char *readonly_text_register::get_string()
{
  return s;
}

readonly_register::readonly_register(int *q) : p(q)
{
}

const char *readonly_register::get_string()
{
  return i_to_a(*p);
}

void abort_request()
{
  int c;
  if (tok.is_eof())
    c = EOF;
  else if (tok.is_newline())
    c = '\n';
  else {
    while ((c = get_copy(0)) == ' ')
      ;
  }
  if (!(c == EOF || c == '\n')) {
    for (; c != '\n' && c != EOF; c = get_copy(0))
      fputs(asciify(c), stderr);
    fputc('\n', stderr);
  }
  fflush(stderr);
  cleanup_and_exit(EXIT_FAILURE);
}

char *read_string()
{
  int len = 256;
  char *s = new char[len];
  int c;
  while ((c = get_copy(0)) == ' ')
    ;
  int i = 0;
  while (c != '\n' && c != EOF) {
    if (!is_invalid_input_char(c)) {
      if (i + 2 > len) {
	char *tem = s;
	s = new char[len*2];
	memcpy(s, tem, len);
	len *= 2;
	delete[] tem;
      }
      s[i++] = c;
    }
    c = get_copy(0);
  }
  s[i] = '\0';
  tok.next();
  if (i == 0) {
    delete[] s;
    return 0;
  }
  return s;
}

void pipe_output()
{
  if (!unsafe_flag) {
    error("'pi' request is not allowed in safer mode");
    skip_line();
  }
  else {
#ifdef POPEN_MISSING
    error("pipes not available on this system");
    skip_line();
#else /* not POPEN_MISSING */
    if (the_output) {
      error("can't pipe: output already started");
      skip_line();
    }
    else {
      char *pc;
      if ((pc = read_string()) == 0)
	error("can't pipe to empty command");
      if (pipe_command) {
	char *s = new char[strlen(pipe_command) + strlen(pc) + 1 + 1];
	strcpy(s, pipe_command);
	strcat(s, "|");
	strcat(s, pc);
	delete[] pipe_command;
	delete[] pc;
	pipe_command = s;
      }
      else
	pipe_command = pc;
    }
#endif /* not POPEN_MISSING */
  }
}

static int system_status;

void system_request()
{
  if (!unsafe_flag) {
    error("'sy' request is not allowed in safer mode");
    skip_line();
  }
  else {
    char *command = read_string();
    if (!command)
      error("empty command");
    else {
      system_status = system(command);
      delete[] command;
    }
  }
}

void copy_file()
{
  if (curdiv == topdiv && topdiv->before_first_page) {
    handle_initial_request(COPY_FILE_REQUEST);
    return;
  }
  symbol filename = get_long_name(true /* required */);
  while (!tok.is_newline() && !tok.is_eof())
    tok.next();
  if (break_flag)
    curenv->do_break();
  if (!filename.is_null())
    curdiv->copy_file(filename.contents());
  tok.next();
}

#ifdef COLUMN

void vjustify()
{
  if (curdiv == topdiv && topdiv->before_first_page) {
    handle_initial_request(VJUSTIFY_REQUEST);
    return;
  }
  symbol type = get_long_name(true /* required */);
  if (!type.is_null())
    curdiv->vjustify(type);
  skip_line();
}

#endif /* COLUMN */

void transparent_file()
{
  if (curdiv == topdiv && topdiv->before_first_page) {
    handle_initial_request(TRANSPARENT_FILE_REQUEST);
    return;
  }
  symbol filename = get_long_name(true /* required */);
  while (!tok.is_newline() && !tok.is_eof())
    tok.next();
  if (break_flag)
    curenv->do_break();
  if (!filename.is_null()) {
    errno = 0;
    FILE *fp = include_search_path.open_file_cautious(filename.contents());
    if (!fp)
      error("can't open '%1': %2", filename.contents(), strerror(errno));
    else {
      int bol = 1;
      for (;;) {
	int c = getc(fp);
	if (c == EOF)
	  break;
	if (is_invalid_input_char(c))
	  warning(WARN_INPUT, "invalid input character code %1", int(c));
	else {
	  curdiv->transparent_output(c);
	  bol = c == '\n';
	}
      }
      if (!bol)
	curdiv->transparent_output('\n');
      fclose(fp);
    }
  }
  tok.next();
}

class page_range {
  int first;
  int last;
public:
  page_range *next;
  page_range(int, int, page_range *);
  int contains(int n);
};

page_range::page_range(int i, int j, page_range *p)
: first(i), last(j), next(p)
{
}

int page_range::contains(int n)
{
  return n >= first && (last <= 0 || n <= last);
}

page_range *output_page_list = 0;

int in_output_page_list(int n)
{
  if (!output_page_list)
    return 1;
  for (page_range *p = output_page_list; p; p = p->next)
    if (p->contains(n))
      return 1;
  return 0;
}

static void parse_output_page_list(char *p)
{
  for (;;) {
    int i;
    if (*p == '-')
      i = 1;
    else if (csdigit(*p)) {
      i = 0;
      do
	i = i*10 + *p++ - '0';
      while (csdigit(*p));
    }
    else
      break;
    int j;
    if (*p == '-') {
      p++;
      j = 0;
      if (csdigit(*p)) {
	do
	  j = j*10 + *p++ - '0';
	while (csdigit(*p));
      }
    }
    else
      j = i;
    if (j == 0)
      last_page_number = -1;
    else if (last_page_number >= 0 && j > last_page_number)
      last_page_number = j;
    output_page_list = new page_range(i, j, output_page_list);
    if (*p != ',')
      break;
    ++p;
  }
  if (*p != '\0') {
    error("bad output page list");
    output_page_list = 0;
  }
}

static FILE *open_mac_file(const char *mac, char **path)
{
  // Try `mac`.tmac first, then tmac.`mac`.  Expect ENOENT errors.
  char *s1 = new char[strlen(mac)+strlen(MACRO_POSTFIX)+1];
  strcpy(s1, mac);
  strcat(s1, MACRO_POSTFIX);
  FILE *fp = mac_path->open_file(s1, path);
  if (!fp && ENOENT != errno)
    error("can't open macro file '%1': %2", s1, strerror(errno));
  delete[] s1;
  if (!fp) {
    char *s2 = new char[strlen(mac)+strlen(MACRO_PREFIX)+1];
    strcpy(s2, MACRO_PREFIX);
    strcat(s2, mac);
    fp = mac_path->open_file(s2, path);
  if (!fp && ENOENT != errno)
      error("can't open macro file '%1': %2", s2, strerror(errno));
    delete[] s2;
  }
  return fp;
}

static void process_macro_file(const char *mac)
{
  char *path;
  FILE *fp = open_mac_file(mac, &path);
  if (!fp)
    fatal("unable to open macro file for -m argument '%1'", mac);
  const char *s = symbol(path).contents();
  free(path);
  input_stack::push(new file_iterator(fp, s));
  tok.next();
  process_input_stack();
}

static void process_startup_file(const char *filename)
{
  char *path;
  search_path *orig_mac_path = mac_path;
  mac_path = &config_macro_path;
  FILE *fp = mac_path->open_file(filename, &path);
  if (fp) {
    input_stack::push(new file_iterator(fp, symbol(path).contents()));
    free(path);
    tok.next();
    process_input_stack();
  }
  mac_path = orig_mac_path;
}

void do_macro_source(bool quietly)
{
  symbol nm = get_long_name(true /* required */);
  if (nm.is_null())
    skip_line();
  else {
    while (!tok.is_newline() && !tok.is_eof())
      tok.next();
    char *path;
    FILE *fp = mac_path->open_file(nm.contents(), &path);
    // .mso cannot go through open_mac_file, which handles the -m option
    // and expects only an identifier like "s" or "an", not a file name.
    // We need to do it here manually: If we have tmac.FOOBAR, try
    // FOOBAR.tmac and vice versa.
    if (!fp) {
      const char *fn = nm.contents();
      size_t fnlen = strlen(fn);
      if (strncasecmp(fn, MACRO_PREFIX, sizeof(MACRO_PREFIX) - 1) == 0) {
	char *s = new char[fnlen + sizeof(MACRO_POSTFIX)];
	strcpy(s, fn + sizeof(MACRO_PREFIX) - 1);
	strcat(s, MACRO_POSTFIX);
	fp = mac_path->open_file(s, &path);
	delete[] s;
      }
      if (!fp) {
	if (strncasecmp(fn + fnlen - sizeof(MACRO_POSTFIX) + 1,
			MACRO_POSTFIX, sizeof(MACRO_POSTFIX) - 1) == 0) {
	  char *s = new char[fnlen + sizeof(MACRO_PREFIX)];
	  strcpy(s, MACRO_PREFIX);
	  strncat(s, fn, fnlen - sizeof(MACRO_POSTFIX) + 1);
	  fp = mac_path->open_file(s, &path);
	  delete[] s;
	}
      }
    }
    if (fp) {
      input_stack::push(new file_iterator(fp, symbol(path).contents()));
      free(path);
    }
    else
      // Suppress diagnostic only if we're operating quietly and it's an
      // expected problem.
      if (!quietly && (ENOENT == errno))
	warning(WARN_FILE, "can't open macro file '%1': %2",
		nm.contents(), strerror(errno));
    tok.next();
  }
}

// .mso

void macro_source()
{
  do_macro_source(false /* not quietly (if WARN_FILE enabled) */ );
}

// .msoquiet: like .mso, but silently ignore files that can't be opened
// due to their nonexistence

void macro_source_quietly()
{
  do_macro_source(true /* quietly */ );
}

static void process_input_file(const char *name)
{
  FILE *fp;
  if (strcmp(name, "-") == 0) {
    clearerr(stdin);
    fp = stdin;
  }
  else {
    errno = 0;
    fp = include_search_path.open_file_cautious(name);
    if (!fp)
      fatal("can't open '%1': %2", name, strerror(errno));
  }
  input_stack::push(new file_iterator(fp, name));
  tok.next();
  process_input_stack();
}

// make sure the_input is empty before calling this

static int evaluate_expression(const char *expr, units *res)
{
  input_stack::push(make_temp_iterator(expr));
  tok.next();
  int success = get_number(res, 'u');
  while (input_stack::get(0) != EOF)
    ;
  return success;
}

static void do_register_assignment(const char *s)
{
  const char *p = strchr(s, '=');
  if (!p) {
    char buf[2];
    buf[0] = s[0];
    buf[1] = 0;
    units n;
    if (evaluate_expression(s + 1, &n))
      set_number_reg(buf, n);
  }
  else {
    char *buf = new char[p - s + 1];
    memcpy(buf, s, p - s);
    buf[p - s] = 0;
    units n;
    if (evaluate_expression(p + 1, &n))
      set_number_reg(buf, n);
    delete[] buf;
  }
}

static void set_string(const char *name, const char *value)
{
  macro *m = new macro;
  for (const char *p = value; *p; p++)
    if (!is_invalid_input_char((unsigned char)*p))
      m->append(*p);
  request_dictionary.define(name, m);
}

static void do_string_assignment(const char *s)
{
  const char *p = strchr(s, '=');
  if (!p) {
    char buf[2];
    buf[0] = s[0];
    buf[1] = 0;
    set_string(buf, s + 1);
  }
  else {
    char *buf = new char[p - s + 1];
    memcpy(buf, s, p - s);
    buf[p - s] = 0;
    set_string(buf, p + 1);
    delete[] buf;
  }
}

struct string_list {
  const char *s;
  string_list *next;
  string_list(const char *ss) : s(ss), next(0) {}
};

#if 0
static void prepend_string(const char *s, string_list **p)
{
  string_list *l = new string_list(s);
  l->next = *p;
  *p = l;
}
#endif

static void add_string(const char *s, string_list **p)
{
  while (*p)
    p = &((*p)->next);
  *p = new string_list(s);
}

void usage(FILE *stream, const char *prog)
{
  fprintf(stream,
"usage: %s [-abcCEiRUz] [-d ct] [-d string=text] [-f font-family]"
" [-F font-directory] [-I inclusion-directory] [-m macro-package]"
" [-M macro-directory] [-n page-number] [-o page-list]"
" [-r cnumeric-expression] [-r register=numeric-expression]"
" [-T output-device] [-w warning-category] [-W warning-category]"
" [file ...]\n"
"usage: %s {-v | --version}\n"
"usage: %s {-h | --help}\n",
	  prog, prog, prog);
}

int main(int argc, char **argv)
{
  program_name = argv[0];
  static char stderr_buf[BUFSIZ];
  setbuf(stderr, stderr_buf);
  int c;
  string_list *macros = 0;
  string_list *register_assignments = 0;
  string_list *string_assignments = 0;
  int iflag = 0;
  int tflag = 0;
  int fflag = 0;
  int nflag = 0;
  int no_rc = 0;		// don't process troffrc and troffrc-end
  int next_page_number = 0;	// pacify compiler
  opterr = 0;
  hresolution = vresolution = 1;
  // restore $PATH if called from groff
  char* groff_path = getenv("GROFF_PATH__");
  if (groff_path) {
    string e = "PATH";
    e += '=';
    if (*groff_path)
      e += groff_path;
    e += '\0';
    if (putenv(strsave(e.contents())))
      fatal("putenv failed");
  }
  setlocale(LC_CTYPE, "");
  static const struct option long_options[] = {
    { "help", no_argument, 0, CHAR_MAX + 1 },
    { "version", no_argument, 0, 'v' },
    { 0, 0, 0, 0 }
  };
#if defined(DEBUGGING)
#define DEBUG_OPTION "D"
#else
#define DEBUG_OPTION ""
#endif
  while ((c = getopt_long(argc, argv,
			  "abciI:vw:W:zCEf:m:n:o:r:d:F:M:T:tqs:RU"
			  DEBUG_OPTION, long_options, 0))
	 != EOF)
    switch(c) {
    case 'v':
      {
	printf("GNU troff (groff) version %s\n", Version_string);
	exit(0);
	break;
      }
    case 'I':
      // Search path for .psbb files
      // and most other non-system input files.
      include_search_path.command_line_dir(optarg);
      break;
    case 'T':
      device = optarg;
      tflag = 1;
      is_html = (strcmp(device, "html") == 0);
      break;
    case 'C':
      compatible_flag = 1;
      // fall through
    case 'c':
      color_flag = 0;
      break;
    case 'M':
      macro_path.command_line_dir(optarg);
      safer_macro_path.command_line_dir(optarg);
      config_macro_path.command_line_dir(optarg);
      break;
    case 'F':
      font::command_line_font_dir(optarg);
      break;
    case 'm':
      add_string(optarg, &macros);
      break;
    case 'E':
      inhibit_errors = 1;
      break;
    case 'R':
      no_rc = 1;
      break;
    case 'w':
      enable_warning(optarg);
      break;
    case 'W':
      disable_warning(optarg);
      break;
    case 'i':
      iflag = 1;
      break;
    case 'b':
      backtrace_flag = 1;
      break;
    case 'a':
      ascii_output_flag = 1;
      break;
    case 'z':
      suppress_output_flag = 1;
      break;
    case 'n':
      if (sscanf(optarg, "%d", &next_page_number) == 1)
	nflag++;
      else
	error("bad page number");
      break;
    case 'o':
      parse_output_page_list(optarg);
      break;
    case 'd':
      if (*optarg == '\0')
	error("'-d' requires non-empty argument");
      else if (*optarg == '=')
	error("malformed argument to '-d'; string name cannot be empty"
	      " or contain an equals sign");
      else
	add_string(optarg, &string_assignments);
      break;
    case 'r':
      if (*optarg == '\0')
	error("'-r' requires non-empty argument");
      else if (*optarg == '=')
	error("malformed argument to '-r'; register name cannot be"
	      " empty or contain an equals sign");
      else
	add_string(optarg, &register_assignments);
      break;
    case 'f':
      default_family = symbol(optarg);
      fflag = 1;
      break;
    case 'q':
    case 's':
    case 't':
      // silently ignore these
      break;
    case 'U':
      unsafe_flag = 1;	// unsafe behaviour
      break;
#if defined(DEBUGGING)
    case 'D':
      debug_state = 1;
      break;
#endif
    case CHAR_MAX + 1: // --help
      usage(stdout, argv[0]);
      exit(0);
      break;
    case '?':
      usage(stderr, argv[0]);
      exit(1);
      break;		// never reached
    default:
      assert(0);
    }
  if (unsafe_flag)
    mac_path = &macro_path;
  set_string(".T", device);
  init_charset_table();
  init_hpf_code_table();
  if (0 /* nullptr */ == font::load_desc())
    fatal("cannot load 'DESC' description file for device '%1'",
	  device);
  units_per_inch = font::res;
  hresolution = font::hor;
  vresolution = font::vert;
  sizescale = font::sizescale;
  device_has_tcommand = font::has_tcommand;
  warn_scale = (double)units_per_inch;
  warn_scaling_indicator = 'i';
  if (!fflag && font::family != 0 && *font::family != '\0')
    default_family = symbol(font::family);
  font_size::init_size_table(font::sizes);
  int i;
  int j = 1;
  if (font::style_table)
    for (i = 0; font::style_table[i]; i++)
      // Mounting a style can't actually fail due to a bad style name;
      // that's not determined until the full font name is resolved.
      // The DESC file also can't provoke a problem by requesting over a
      // thousand slots in the style table.
      if (!mount_style(j++, symbol(font::style_table[i])))
	warning(WARN_FONT, "cannot mount style '%1' directed by 'DESC'"
		" file for device '%2'", font::style_table[i], device);
  for (i = 0; font::font_name_table[i]; i++, j++)
    // In the DESC file, a font name of 0 (zero) means "leave this
    // position empty".
    if (strcmp(font::font_name_table[i], "0") != 0)
      if (!mount_font(j, symbol(font::font_name_table[i])))
	warning(WARN_FONT, "cannot mount font '%1' directed by 'DESC'"
		" file for device '%2'", font::font_name_table[i],
		device);
  curdiv = topdiv = new top_level_diversion;
  if (nflag)
    topdiv->set_next_page_number(next_page_number);
  init_input_requests();
  init_env_requests();
  init_div_requests();
#ifdef COLUMN
  init_column_requests();
#endif /* COLUMN */
  init_node_requests();
  register_dictionary.define(".T", new readonly_text_register(tflag ? "1" : "0"));
  init_registers();
  init_reg_requests();
  init_hyphen_requests();
  init_environments();
  while (string_assignments) {
    do_string_assignment(string_assignments->s);
    string_list *tem = string_assignments;
    string_assignments = string_assignments->next;
    delete tem;
  }
  while (register_assignments) {
    do_register_assignment(register_assignments->s);
    string_list *tem = register_assignments;
    register_assignments = register_assignments->next;
    delete tem;
  }
  if (!no_rc)
    process_startup_file(INITIAL_STARTUP_FILE);
  while (macros) {
    process_macro_file(macros->s);
    string_list *tem = macros;
    macros = macros->next;
    delete tem;
  }
  if (!no_rc)
    process_startup_file(FINAL_STARTUP_FILE);
  for (i = optind; i < argc; i++)
    process_input_file(argv[i]);
  if (optind >= argc || iflag)
    process_input_file("-");
  exit_troff();
  return 0;			// not reached
}

void warn_request()
{
  int n;
  if (has_arg() && get_integer(&n)) {
    if (n & ~WARN_TOTAL) {
      warning(WARN_RANGE, "warning mask must be between 0 and %1", WARN_TOTAL);
      n &= WARN_TOTAL;
    }
    warning_mask = n;
  }
  else
    warning_mask = WARN_TOTAL;
  skip_line();
}

static void init_registers()
{
#ifdef LONG_FOR_TIME_T
  long
#else /* not LONG_FOR_TIME_T */
  time_t
#endif /* not LONG_FOR_TIME_T */
    t = current_time();
  struct tm *tt = localtime(&t);
  set_number_reg("seconds", int(tt->tm_sec));
  set_number_reg("minutes", int(tt->tm_min));
  set_number_reg("hours", int(tt->tm_hour));
  set_number_reg("dw", int(tt->tm_wday + 1));
  set_number_reg("dy", int(tt->tm_mday));
  set_number_reg("mo", int(tt->tm_mon + 1));
  set_number_reg("year", int(1900 + tt->tm_year));
  set_number_reg("yr", int(tt->tm_year));
  set_number_reg("$$", getpid());
  register_dictionary.define(".A",
			       new readonly_text_register(ascii_output_flag
						? "1"
						: "0"));
}

/*
 *  registers associated with \O
 */

static int output_reg_minx_contents = -1;
static int output_reg_miny_contents = -1;
static int output_reg_maxx_contents = -1;
static int output_reg_maxy_contents = -1;

void check_output_limits(int x, int y)
{
  if ((output_reg_minx_contents == -1) || (x < output_reg_minx_contents))
    output_reg_minx_contents = x;
  if (x > output_reg_maxx_contents)
    output_reg_maxx_contents = x;
  if ((output_reg_miny_contents == -1) || (y < output_reg_miny_contents))
    output_reg_miny_contents = y;
  if (y > output_reg_maxy_contents)
    output_reg_maxy_contents = y;
}

void reset_output_registers()
{
  output_reg_minx_contents = -1;
  output_reg_miny_contents = -1;
  output_reg_maxx_contents = -1;
  output_reg_maxy_contents = -1;
}

void get_output_registers(int *minx, int *miny, int *maxx, int *maxy)
{
  *minx = output_reg_minx_contents;
  *miny = output_reg_miny_contents;
  *maxx = output_reg_maxx_contents;
  *maxy = output_reg_maxy_contents;
}

void init_input_requests()
{
  init_request("ab", abort_request);
  init_request("als", alias_macro);
  init_request("am", append_macro);
  init_request("am1", append_nocomp_macro);
  init_request("ami", append_indirect_macro);
  init_request("ami1", append_indirect_nocomp_macro);
  init_request("as", append_string);
  init_request("as1", append_nocomp_string);
  init_request("asciify", asciify_macro);
  init_request("backtrace", backtrace_request);
  init_request("blm", blank_line_macro);
  init_request("break", while_break_request);
  init_request("cf", copy_file);
  init_request("cflags", char_flags);
  init_request("char", define_character);
  init_request("chop", chop_macro);
  init_request("class", define_class);
  init_request("close", close_request);
  init_request("color", activate_color);
  init_request("composite", composite_request);
  init_request("continue", while_continue_request);
  init_request("cp", compatible);
  init_request("de", define_macro);
  init_request("de1", define_nocomp_macro);
  init_request("defcolor", define_color);
  init_request("dei", define_indirect_macro);
  init_request("dei1", define_indirect_nocomp_macro);
  init_request("device", device_request);
  init_request("devicem", device_macro_request);
  init_request("do", do_request);
  init_request("ds", define_string);
  init_request("ds1", define_nocomp_string);
  init_request("ec", set_escape_char);
  init_request("ecr", restore_escape_char);
  init_request("ecs", save_escape_char);
  init_request("el", else_request);
  init_request("em", eoi_macro);
  init_request("eo", escape_off);
  init_request("ex", exit_request);
  init_request("fchar", define_fallback_character);
#ifdef WIDOW_CONTROL
  init_request("fpl", flush_pending_lines);
#endif /* WIDOW_CONTROL */
  init_request("hcode", hyphenation_code);
  init_request("hpfcode", hyphenation_patterns_file_code);
  init_request("ie", if_else_request);
  init_request("if", if_request);
  init_request("ig", ignore);
  init_request("length", length_request);
  init_request("lf", line_file);
  init_request("lsm", leading_spaces_macro);
  init_request("mso", macro_source);
  init_request("msoquiet", macro_source_quietly);
  init_request("nop", nop_request);
  init_request("nroff", nroff_request);
  init_request("nx", next_file);
  init_request("open", open_request);
  init_request("opena", opena_request);
  init_request("output", output_request);
  init_request("pc", set_page_character);
  init_request("pi", pipe_output);
  init_request("pm", print_macros);
  init_request("psbb", ps_bbox_request);
#ifndef POPEN_MISSING
  init_request("pso", pipe_source);
#endif /* not POPEN_MISSING */
  init_request("rchar", remove_character);
  init_request("rd", read_request);
  init_request("return", return_macro_request);
  init_request("rm", remove_macro);
  init_request("rn", rename_macro);
  init_request("schar", define_special_character);
  init_request("shift", shift);
  init_request("so", source);
  init_request("soquiet", source_quietly);
  init_request("spreadwarn", spreadwarn_request);
  init_request("stringdown", stringdown_request);
  init_request("stringup", stringup_request);
  init_request("substring", substring_request);
  init_request("sy", system_request);
  init_request("tag", tag);
  init_request("taga", taga);
  init_request("tm", terminal);
  init_request("tm1", terminal1);
  init_request("tmc", terminal_continue);
  init_request("tr", translate);
  init_request("trf", transparent_file);
  init_request("trin", translate_input);
  init_request("trnt", translate_no_transparent);
  init_request("troff", troff_request);
  init_request("unformat", unformat_macro);
#ifdef COLUMN
  init_request("vj", vjustify);
#endif /* COLUMN */
  init_request("warn", warn_request);
  init_request("warnscale", warnscale_request);
  init_request("while", while_request);
  init_request("write", write_request);
  init_request("writec", write_request_continue);
  init_request("writem", write_macro_request);
  register_dictionary.define(".$", new nargs_reg);
  register_dictionary.define(".br", new break_flag_reg);
  register_dictionary.define(".C", new readonly_register(&compatible_flag));
  register_dictionary.define(".cp", new readonly_register(&do_old_compatible_flag));
  register_dictionary.define(".O", new variable_reg(&begin_level));
  register_dictionary.define(".c", new lineno_reg);
  register_dictionary.define(".color", new readonly_register(&color_flag));
  register_dictionary.define(".F", new filename_reg);
  register_dictionary.define(".g", new readonly_text_register("1"));
  register_dictionary.define(".H", new readonly_register(&hresolution));
  register_dictionary.define(".R", new readonly_text_register("10000"));
  register_dictionary.define(".U", new readonly_register(&unsafe_flag));
  register_dictionary.define(".V", new readonly_register(&vresolution));
  register_dictionary.define(".warn", new readonly_register(&warning_mask));
  extern const char *major_version;
  register_dictionary.define(".x", new readonly_text_register(major_version));
  extern const char *revision;
  register_dictionary.define(".Y", new readonly_text_register(revision));
  extern const char *minor_version;
  register_dictionary.define(".y", new readonly_text_register(minor_version));
  register_dictionary.define("c.", new writable_lineno_reg);
  register_dictionary.define("llx", new variable_reg(&llx_reg_contents));
  register_dictionary.define("lly", new variable_reg(&lly_reg_contents));
  register_dictionary.define("lsn", new variable_reg(&leading_spaces_number));
  register_dictionary.define("lss", new variable_reg(&leading_spaces_space));
  register_dictionary.define("opmaxx",
			       new variable_reg(&output_reg_maxx_contents));
  register_dictionary.define("opmaxy",
			       new variable_reg(&output_reg_maxy_contents));
  register_dictionary.define("opminx",
			       new variable_reg(&output_reg_minx_contents));
  register_dictionary.define("opminy",
			       new variable_reg(&output_reg_miny_contents));
  register_dictionary.define("slimit",
			       new variable_reg(&input_stack::limit));
  register_dictionary.define("systat", new variable_reg(&system_status));
  register_dictionary.define("urx", new variable_reg(&urx_reg_contents));
  register_dictionary.define("ury", new variable_reg(&ury_reg_contents));
}

object_dictionary request_dictionary(501);

void init_request(const char *s, REQUEST_FUNCP f)
{
  request_dictionary.define(s, new request(f));
}

static request_or_macro *lookup_request(symbol nm)
{
  assert(!nm.is_null());
  request_or_macro *p = (request_or_macro *)request_dictionary.lookup(nm);
  if (p == 0) {
    warning(WARN_MAC, "macro '%1' not defined", nm.contents());
    p = new macro;
    request_dictionary.define(nm, p);
  }
  return p;
}

node *charinfo_to_node_list(charinfo *ci, const environment *envp)
{
  // Don't interpret character definitions in compatible mode.
  int old_compatible_flag = compatible_flag;
  compatible_flag = 0;
  int old_escape_char = escape_char;
  escape_char = '\\';
  macro *mac = ci->set_macro(0);
  assert(mac != 0);
  environment *oldenv = curenv;
  environment env(envp);
  curenv = &env;
  curenv->set_composite();
  token old_tok = tok;
  input_stack::add_boundary();
  string_iterator *si =
    new string_iterator(*mac, "composite character", ci->nm);
  input_stack::push(si);
  // we don't use process_input_stack, because we don't want to recognise
  // requests
  for (;;) {
    tok.next();
    if (tok.is_eof())
      break;
    if (tok.is_newline()) {
      error("composite character mustn't contain newline");
      while (!tok.is_eof())
	tok.next();
      break;
    }
    else
      tok.process();
  }
  node *n = curenv->extract_output_line();
  input_stack::remove_boundary();
  ci->set_macro(mac);
  tok = old_tok;
  curenv = oldenv;
  compatible_flag = old_compatible_flag;
  escape_char = old_escape_char;
  have_input = 0;
  return n;
}

static node *read_draw_node()
{
  token start;
  start.next();
  if (!start.usable_as_delimiter(true /* report error */)){
    do {
      tok.next();
    } while (tok != start && !tok.is_newline() && !tok.is_eof());
  }
  else {
    tok.next();
    if (tok == start)
      error("missing argument");
    else {
      unsigned char type = tok.ch();
      if (type == 'F') {
	read_color_draw_node(start);
	return 0;
      }
      tok.next();
      int maxpoints = 10;
      hvpair *point = new hvpair[maxpoints];
      int npoints = 0;
      int no_last_v = 0;
      bool err = false;
      int i;
      for (i = 0; tok != start; i++) {
	if (i == maxpoints) {
	  hvpair *oldpoint = point;
	  point = new hvpair[maxpoints*2];
	  for (int j = 0; j < maxpoints; j++)
	    point[j] = oldpoint[j];
	  maxpoints *= 2;
	  delete[] oldpoint;
	}
	if (tok.is_newline() || tok.is_eof()) {
	  warning(WARN_DELIM, "missing closing delimiter in drawing"
		  " escape sequence (got %1)", tok.description());
	  err = true;
	  break;
	}
	if (!get_hunits(&point[i].h,
			type == 'f' || type == 't' ? 'u' : 'm')) {
	  err = true;
	  break;
	}
	++npoints;
	tok.skip();
	point[i].v = V0;
	if (tok == start) {
	  no_last_v = 1;
	  break;
	}
	if (!get_vunits(&point[i].v, 'v')) {
	  err = false;
	  break;
	}
	tok.skip();
      }
      while (tok != start && !tok.is_newline() && !tok.is_eof())
	tok.next();
      if (!err) {
	switch (type) {
	case 'l':
	  if (npoints != 1 || no_last_v) {
	    error("two arguments needed for line");
	    npoints = 1;
	  }
	  break;
	case 'c':
	  if (npoints != 1 || !no_last_v) {
	    error("one argument needed for circle");
	    npoints = 1;
	    point[0].v = V0;
	  }
	  break;
	case 'e':
	  if (npoints != 1 || no_last_v) {
	    error("two arguments needed for ellipse");
	    npoints = 1;
	  }
	  break;
	case 'a':
	  if (npoints != 2 || no_last_v) {
	    error("four arguments needed for arc");
	    npoints = 2;
	  }
	  break;
	case '~':
	  if (no_last_v)
	    error("even number of arguments needed for spline");
	  break;
	case 'f':
	  if (npoints != 1 || !no_last_v) {
	    error("one argument needed for gray shade");
	    npoints = 1;
	    point[0].v = V0;
	  }
	default:
	  // silently pass it through
	  break;
	}
	draw_node *dn = new draw_node(type, point, npoints,
				      curenv->get_font_size(),
				      curenv->get_glyph_color(),
				      curenv->get_fill_color());
	delete[] point;
	return dn;
      }
      else {
	delete[] point;
      }
    }
  }
  return 0;
}

static void read_color_draw_node(token &start)
{
  tok.next();
  if (tok == start) {
    error("missing color scheme");
    return;
  }
  unsigned char scheme = tok.ch();
  tok.next();
  color *col = 0;
  char end = start.ch();
  switch (scheme) {
  case 'c':
    col = read_cmy(end);
    break;
  case 'd':
    col = &default_color;
    break;
  case 'g':
    col = read_gray(end);
    break;
  case 'k':
    col = read_cmyk(end);
    break;
  case 'r':
    col = read_rgb(end);
    break;
  }
  if (col)
    curenv->set_fill_color(col);
  while (tok != start) {
    if (tok.is_newline() || tok.is_eof()) {
      warning(WARN_DELIM, "missing closing delimiter in color space"
	      " drawing escape sequence (got %1)", tok.description());
      input_stack::push(make_temp_iterator("\n"));
      break;
    }
    tok.next();
  }
  have_input = 1;
}

static struct {
  const char *name;
  int mask;
} warning_table[] = {
  { "char", WARN_CHAR },
  { "range", WARN_RANGE },
  { "break", WARN_BREAK },
  { "delim", WARN_DELIM },
  { "el", WARN_EL },
  { "scale", WARN_SCALE },
  { "number", WARN_NUMBER },
  { "syntax", WARN_SYNTAX },
  { "tab", WARN_TAB },
  { "right-brace", WARN_RIGHT_BRACE },
  { "missing", WARN_MISSING },
  { "input", WARN_INPUT },
  { "escape", WARN_ESCAPE },
  { "space", WARN_SPACE },
  { "font", WARN_FONT },
  { "di", WARN_DI },
  { "mac", WARN_MAC },
  { "reg", WARN_REG },
  { "ig", WARN_IG },
  { "color", WARN_COLOR },
  { "file", WARN_FILE },
  { "all", WARN_TOTAL & ~(WARN_DI | WARN_MAC | WARN_REG) },
  { "w", WARN_TOTAL },
  { "default", DEFAULT_WARNING_MASK },
};

static int lookup_warning(const char *name)
{
  for (unsigned int i = 0;
       i < sizeof(warning_table)/sizeof(warning_table[0]);
       i++)
    if (strcmp(name, warning_table[i].name) == 0)
      return warning_table[i].mask;
  return 0;
}

static void enable_warning(const char *name)
{
  int mask = lookup_warning(name);
  if (mask)
    warning_mask |= mask;
  else
    error("unrecognized warning category '%1'", name);
}

static void disable_warning(const char *name)
{
  int mask = lookup_warning(name);
  if (mask)
    warning_mask &= ~mask;
  else
    error("unrecognized warning category '%1'", name);
}

static void copy_mode_error(const char *format,
			    const errarg &arg1,
			    const errarg &arg2,
			    const errarg &arg3)
{
  if (ignoring) {
    static const char prefix[] = "(in ignored input) ";
    char *s = new char[sizeof(prefix) + strlen(format)];
    strcpy(s, prefix);
    strcat(s, format);
    warning(WARN_IG, s, arg1, arg2, arg3);
    delete[] s;
  }
  else
    error(format, arg1, arg2, arg3);
}

enum error_type { DEBUG, WARNING, OUTPUT_WARNING, ERROR, FATAL };

static void do_error(error_type type,
		     const char *format,
		     const errarg &arg1,
		     const errarg &arg2,
		     const errarg &arg3)
{
  const char *filename;
  int lineno;
  if (inhibit_errors && type < FATAL)
    return;
  if (backtrace_flag)
    input_stack::backtrace();
  if (!get_file_line(&filename, &lineno))
    filename = 0;
  if (filename) {
    if (program_name)
      errprint("%1:", program_name);
    errprint("%1:%2: ", filename, lineno);
  }
  else if (program_name)
    fprintf(stderr, "%s: ", program_name);
  switch (type) {
  case FATAL:
    fputs("fatal error: ", stderr);
    break;
  case ERROR:
    fputs("error: ", stderr);
    break;
  case WARNING:
    fputs("warning: ", stderr);
    break;
  case DEBUG:
    fputs("debug: ", stderr);
    break;
  case OUTPUT_WARNING:
    double fromtop = topdiv->get_vertical_position().to_units() / warn_scale;
    fprintf(stderr, "warning [p %d, %.1f%c",
	    topdiv->get_page_number(), fromtop, warn_scaling_indicator);
    if (topdiv != curdiv) {
      double fromtop1 = curdiv->get_vertical_position().to_units()
			/ warn_scale;
      fprintf(stderr, ", div '%s', %.1f%c",
	      curdiv->get_diversion_name(), fromtop1, warn_scaling_indicator);
    }
    fprintf(stderr, "]: ");
    break;
  }
  errprint(format, arg1, arg2, arg3);
  fputc('\n', stderr);
  fflush(stderr);
  if (type == FATAL)
    cleanup_and_exit(EXIT_FAILURE);
}

void debug(const char *format,
	   const errarg &arg1,
	   const errarg &arg2,
	   const errarg &arg3)
{
  do_error(DEBUG, format, arg1, arg2, arg3);
}

int warning(warning_type t,
	    const char *format,
	    const errarg &arg1,
	    const errarg &arg2,
	    const errarg &arg3)
{
  if ((t & warning_mask) != 0) {
    do_error(WARNING, format, arg1, arg2, arg3);
    return 1;
  }
  else
    return 0;
}

int output_warning(warning_type t,
		   const char *format,
		   const errarg &arg1,
		   const errarg &arg2,
		   const errarg &arg3)
{
  if ((t & warning_mask) != 0) {
    do_error(OUTPUT_WARNING, format, arg1, arg2, arg3);
    return 1;
  }
  else
    return 0;
}

void error(const char *format,
	   const errarg &arg1,
	   const errarg &arg2,
	   const errarg &arg3)
{
  do_error(ERROR, format, arg1, arg2, arg3);
}

void fatal(const char *format,
	   const errarg &arg1,
	   const errarg &arg2,
	   const errarg &arg3)
{
  do_error(FATAL, format, arg1, arg2, arg3);
}

void fatal_with_file_and_line(const char *filename, int lineno,
			      const char *format,
			      const errarg &arg1,
			      const errarg &arg2,
			      const errarg &arg3)
{
  if (program_name)
    fprintf(stderr, "%s:", program_name);
  fprintf(stderr, "%s:", filename);
  if (lineno > 0)
    fprintf(stderr, "%d:", lineno);
  fputs(" fatal error: ", stderr);
  errprint(format, arg1, arg2, arg3);
  fputc('\n', stderr);
  fflush(stderr);
  cleanup_and_exit(EXIT_FAILURE);
}

void error_with_file_and_line(const char *filename, int lineno,
			      const char *format,
			      const errarg &arg1,
			      const errarg &arg2,
			      const errarg &arg3)
{
  if (program_name)
    fprintf(stderr, "%s:", program_name);
  fprintf(stderr, "%s:", filename);
  if (lineno > 0)
    fprintf(stderr, "%d:", lineno);
  fputs(" error: ", stderr);
  errprint(format, arg1, arg2, arg3);
  fputc('\n', stderr);
  fflush(stderr);
}

void debug_with_file_and_line(const char *filename,
			      int lineno,
			      const char *format,
			      const errarg &arg1,
			      const errarg &arg2,
			      const errarg &arg3)
{
  if (program_name)
    fprintf(stderr, "%s:", program_name);
  fprintf(stderr, "%s:", filename);
  if (lineno > 0)
    fprintf(stderr, "%d:", lineno);
  fputs(" debug: ", stderr);
  errprint(format, arg1, arg2, arg3);
  fputc('\n', stderr);
  fflush(stderr);
}

dictionary charinfo_dictionary(501);

charinfo *get_charinfo(symbol nm)
{
  void *p = charinfo_dictionary.lookup(nm);
  if (p != 0)
    return (charinfo *)p;
  charinfo *cp = new charinfo(nm);
  (void)charinfo_dictionary.lookup(nm, cp);
  return cp;
}

int charinfo::next_index = 0;

charinfo::charinfo(symbol s)
: translation(0), mac(0), special_translation(TRANSLATE_NONE),
  hyphenation_code(0), flags(0), ascii_code(0), asciify_code(0),
  not_found(0), transparent_translate(1), translate_input(0),
  mode(CHAR_NORMAL), nm(s)
{
  index = next_index++;
  number = -1;
  get_flags();
}

int charinfo::get_unicode_code()
{
  if (ascii_code != '\0')
    return ascii_code;
  return glyph_to_unicode(this);
}

void charinfo::set_hyphenation_code(unsigned char c)
{
  hyphenation_code = c;
}

void charinfo::set_translation(charinfo *ci, int tt, int ti)
{
  translation = ci;
  if (ci && ti) {
    if (hyphenation_code != 0)
      ci->set_hyphenation_code(hyphenation_code);
    if (asciify_code != 0)
      ci->set_asciify_code(asciify_code);
    else if (ascii_code != 0)
      ci->set_asciify_code(ascii_code);
    ci->set_translation_input();
  }
  special_translation = TRANSLATE_NONE;
  transparent_translate = tt;
}

// Recompute flags for all entries in the charinfo dictionary.
void get_flags()
{
  dictionary_iterator iter(charinfo_dictionary);
  charinfo *ci;
  symbol s;
  while (iter.get(&s, (void **)&ci)) {
    assert(!s.is_null());
    ci->get_flags();
  }
  class_flag = 0;
}

// Get the union of all flags affecting this charinfo.
void charinfo::get_flags()
{
  dictionary_iterator iter(char_class_dictionary);
  charinfo *ci;
  symbol s;
  while (iter.get(&s, (void **)&ci)) {
    assert(!s.is_null());
    if (ci->contains(get_unicode_code())) {
#if defined(DEBUGGING)
      if (debug_state)
	fprintf(stderr, "charinfo::get_flags %p %s %d\n",
			(void *)ci, ci->nm.contents(), ci->flags);
#endif
      flags |= ci->flags;
    }
  }
}

void charinfo::set_special_translation(int c, int tt)
{
  special_translation = c;
  translation = 0;
  transparent_translate = tt;
}

void charinfo::set_ascii_code(unsigned char c)
{
  ascii_code = c;
}

void charinfo::set_asciify_code(unsigned char c)
{
  asciify_code = c;
}

macro *charinfo::set_macro(macro *m)
{
  macro *tem = mac;
  mac = m;
  return tem;
}

macro *charinfo::setx_macro(macro *m, char_mode cm)
{
  macro *tem = mac;
  mac = m;
  mode = cm;
  return tem;
}

void charinfo::set_number(int n)
{
  assert(n >= 0);
  number = n;
}

int charinfo::get_number()
{
  assert(number >= 0);
  return number;
}

bool charinfo::contains(int c, bool already_called)
{
  if (already_called) {
    warning(WARN_SYNTAX,
	    "cyclic nested class detected while processing character code %1",
	    c);
    return false;
  }
  std::vector<std::pair<int, int> >::const_iterator ranges_iter;
  ranges_iter = ranges.begin();
  while (ranges_iter != ranges.end()) {
    if (c >= ranges_iter->first && c <= ranges_iter->second) {
#if defined(DEBUGGING)
      if (debug_state)
	fprintf(stderr, "charinfo::contains(%d)\n", c);
#endif
      return true;
    }
    ++ranges_iter;
  }

  std::vector<charinfo *>::const_iterator nested_iter;
  nested_iter = nested_classes.begin();
  while (nested_iter != nested_classes.end()) {
    if ((*nested_iter)->contains(c, true))
      return true;
    ++nested_iter;
  }

  return false;
}

bool charinfo::contains(symbol s, bool already_called)
{
  if (already_called) {
    warning(WARN_SYNTAX,
	    "cyclic nested class detected while processing symbol %1",
	    s.contents());
    return false;
  }
  const char *unicode = glyph_name_to_unicode(s.contents());
  if (unicode != 0 && strchr(unicode, '_') == 0) {
    char *ignore;
    int c = (int)strtol(unicode, &ignore, 16);
    return contains(c, true);
  }
  else
    return false;
}

bool charinfo::contains(charinfo *, bool)
{
  // TODO
  return false;
}

symbol UNNAMED_SYMBOL("---");

// For numbered characters not between 0 and 255, we make a symbol out
// of the number and store them in this dictionary.

dictionary numbered_charinfo_dictionary(11);

charinfo *get_charinfo_by_number(int n)
{
  static charinfo *number_table[256];

  if (n >= 0 && n < 256) {
    charinfo *ci = number_table[n];
    if (!ci) {
      ci = new charinfo(UNNAMED_SYMBOL);
      ci->set_number(n);
      number_table[n] = ci;
    }
    return ci;
  }
  else {
    symbol ns(i_to_a(n));
    charinfo *ci = (charinfo *)numbered_charinfo_dictionary.lookup(ns);
    if (!ci) {
      ci = new charinfo(UNNAMED_SYMBOL);
      ci->set_number(n);
      (void)numbered_charinfo_dictionary.lookup(ns, ci);
    }
    return ci;
  }
}

// This overrides the same function from libgroff; while reading font
// definition files it puts single-letter glyph names into
// 'charset_table' and converts glyph names of the form '\x' ('x' a
// single letter) into 'x'.  Consequently, symbol("x") refers to glyph
// name '\x', not 'x'.

glyph *name_to_glyph(const char *nm)
{
  charinfo *ci;
  if (nm[1] == 0)
    ci = charset_table[nm[0] & 0xff];
  else if (nm[0] == '\\' && nm[2] == 0)
    ci = get_charinfo(symbol(nm + 1));
  else
    ci = get_charinfo(symbol(nm));
  return ci->as_glyph();
}

glyph *number_to_glyph(int n)
{
  return get_charinfo_by_number(n)->as_glyph();
}

const char *glyph_to_name(glyph *g)
{
  charinfo *ci = (charinfo *)g; // Every glyph is actually a charinfo.
  return (ci->nm != UNNAMED_SYMBOL ? ci->nm.contents() : 0);
}

// Local Variables:
// fill-column: 72
// mode: C++
// End:
// vim: set cindent noexpandtab shiftwidth=2 textwidth=72:
