input.cpp   [plain text]


// -*- C++ -*-

// <groff_src_dir>/src/libs/libdriver/input.cpp

/* Copyright (C) 1989, 1990, 1991, 1992, 2001, 2002, 2003, 2004, 2005
   Free Software Foundation, Inc.

   Written by James Clark (jjc@jclark.com)
   Major rewrite 2001 by Bernd Warken (bwarken@mayn.de)

   Last update: 15 Jun 2005

   This file is part of groff, the GNU roff text processing system.

   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 2, 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 groff; see the file COPYING.  If not, write to the Free
   Software Foundation, 51 Franklin St - Fifth Floor, Boston, MA
   02110-1301, USA.
*/

/* Description

   This file implements the parser for the intermediate groff output,
   see groff_out(5), and does the printout for the given device.

   All parsed information is processed within the function do_file().
   A device postprocessor just needs to fill in the methods for the class
   `printer' (or rather a derived class) without having to worry about
   the syntax of the intermediate output format.  Consequently, the
   programming of groff postprocessors is similar to the development of
   device drivers.

   The prototyping for this file is done in driver.h (and error.h).
*/

/* Changes of the 2001 rewrite of this file.

   The interface to the outside and the handling of the global
   variables was not changed, but internally many necessary changes
   were performed.

   The main aim for this rewrite is to provide a first step towards
   making groff fully compatible with classical troff without pain.

   Bugs fixed
   - Unknown subcommands of `D' and `x' are now ignored like in the
     classical case, but a warning is issued.  This was also
     implemented for the other commands.
   - A warning is emitted if `x stop' is missing.
   - `DC' and `DE' commands didn't position to the right end after
     drawing (now they do), see discussion below.
   - So far, `x stop' was ignored.  Now it terminates the processing
     of the current intermediate output file like the classical troff.
   - The command `c' didn't check correctly on white-space.
   - The environment stack wasn't suitable for the color extensions
     (replaced by a class).
   - The old groff parser could only handle a prologue with the first
     3 lines having a fixed structure, while classical troff specified
     the sequence of the first 3 commands without further
     restrictions.  Now the parser is smart about additional
     white space, comments, and empty lines in the prologue.
   - The old parser allowed space characters only as syntactical
     separators, while classical troff had tab characters as well.
     Now any sequence of tabs and/or spaces is a syntactical
     separator between commands and/or arguments.
   - Range checks for numbers implemented.

   New and improved features
   - The color commands `m' and `DF' are added.
   - The old color command `Df' is now converted and delegated to `DFg'.
   - The command `F' is implemented as `use intended file name'.  It
     checks whether its argument agrees with the file name used so far,
     otherwise a warning is issued.  Then the new name is remembered
     and used for the following error messages.
   - For the positioning after drawing commands, an alternative, easier
     scheme is provided, but not yet activated; it can be chosen by
     undefining the preprocessor macro STUPID_DRAWING_POSITIONING.
     It extends the rule of the classical troff output language in a
     logical way instead of the rather strange actual positioning.
     For details, see the discussion below.
   - For the `D' commands that only set the environment, the calling of
     pr->send_draw() was removed because this doesn't make sense for
     the `DF' commands; the (changed) environment is sent with the
     next command anyway.
   - Error handling was clearly separated into warnings and fatal.
   - The error behavior on additional arguments for `D' and `x'
     commands with a fixed number of arguments was changed from being
     ignored (former groff) to issue a warning and ignore (now), see
     skip_line_x().  No fatal was chosen because both string and
     integer arguments can occur.
   - The gtroff program issues a trailing dummy integer argument for
     some drawing commands with an odd number of arguments to make the
     number of arguments even, e.g. the DC and Dt commands; this is
     honored now.
   - All D commands with a variable number of args expect an even
     number of trailing integer arguments, so fatal on error was
     implemented.
   - Disable environment stack and the commands `{' and `}' by making
     them conditional on macro USE_ENV_STACK; actually, this is
     undefined by default.  There isn't any known application for these
     features.

   Cosmetics
   - Nested `switch' commands are avoided by using more functions.
     Dangerous 'fall-through's avoided.
   - Commands and functions are sorted alphabetically (where possible).
   - Dynamic arrays/buffers are now implemented as container classes.
   - Some functions had an ugly return structure; this has been
     streamlined by using classes.
   - Use standard C math functions for number handling, so getting rid
     of differences to '0'.
   - The macro `IntArg' has been created for an easier transition
     to guaranteed 32 bits integers (`int' is enough for GNU, while
     ANSI only guarantees `long int' to have a length of 32 bits).
   - The many usages of type `int' are differentiated by using `Char',
     `bool', and `IntArg' where appropriate.
   - To ease the calls of the local utility functions, the parser
     variables `current_file', `npages', and `current_env'
     (formerly env) were made global to the file (formerly they were
     local to the do_file() function)
   - Various comments were added.

   TODO
   - Get rid of the stupid drawing positioning.
   - Can the `Dt' command be completely handled by setting environment
     within do_file() instead of sending to pr?
   - Integer arguments must be >= 32 bits, use conditional #define.
   - Add scaling facility for classical device independence and
     non-groff devices.  Classical troff output had a quasi device
     independence by scaling the intermediate output to the resolution
     of the postprocessor device if different from the one specified
     with `x T', groff have not.  So implement full quasi device
     indepedence, including the mapping of the strange classical
     devices to the postprocessor device (seems to be reasonably
     easy).
   - The external, global pointer variables are not optimally handled.
     - The global variables `current_filename',
       `current_source_filename', and `current_lineno' are only used for
       error reporting.  So implement a static class `Error'
       (`::' calls).
     - The global `device' is the name used during the formatting
       process; there should be a new variable for the device name used
       during the postprocessing.
  - Implement the B-spline drawing `D~' for all graphical devices.
  - Make `environment' a class with an overflow check for its members
    and a delete method to get rid of delete_current_env().
  - Implement the `EnvStack' to use `new' instead of `malloc'.
  - The class definitions of this document could go into a new file.
  - The comments in this section should go to a `Changelog' or some
    `README' file in this directory.
*/

/*
  Discussion of the positioning by drawing commands

  There was some confusion about the positioning of the graphical
  pointer at the printout after having executed a `D' command.
  The classical troff manual of Osanna & Kernighan specified,

    `The position after a graphical object has been drawn is
     at its end; for circles and ellipses, the "end" is at the
     right side.'

  From this, it follows that
  - all open figures (args, splines, and lines) should position at their
    final point.
  - all circles and ellipses should position at their right-most point
    (as if 2 halves had been drawn).
  - all closed figures apart from circles and ellipses shouldn't change
    the position because they return to their origin.
  - all setting commands should not change position because they do not
    draw any graphical object.

  In the case of the open figures, this means that the horizontal
  displacement is the sum of all odd arguments and the vertical offset
  the sum of all even arguments, called the alternate arguments sum
  displacement in the following.

  Unfortunately, groff did not implement this simple rule.  The former
  documentation in groff_out(5) differed from the source code, and
  neither of them is compatible with the classical rule.

  The former groff_out(5) specified to use the alternative arguments
  sum displacement for calculating the drawing positioning of
  non-classical commands, including the `Dt' command (setting-only)
  and closed polygons.  Applying this to the new groff color commands
  will lead to disaster.  For their arguments can take large values (>
  65000).  On low resolution devices, the displacement of such large
  values will corrupt the display or kill the printer.  So the
  nonsense specification has come to a natural end anyway.

  The groff source code, however, had no positioning for the
  setting-only commands (esp. `Dt'), the right-end positioning for
  outlined circles and ellipses, and the alternative argument sum
  displacement for all other commands (including filled circles and
  ellipses).

  The reason why no one seems to have suffered from this mayhem so
  far is that the graphical objects are usually generated by
  preprocessors like pic that do not depend on the automatic
  positioning.  When using the low level `\D' escape sequences or `D'
  output commands, the strange positionings can be circumvented by
  absolute positionings or by tricks like `\Z'.

  So doing an exorcism on the strange, incompatible displacements might
  not harm any existing documents, but will make the usage of the
  graphical escape sequences and commands natural.

  That's why the rewrite of this file returned to the reasonable,
  classical specification with its clear end-of-drawing rule that is
  suitable for all cases.  But a macro STUPID_DRAWING_POSITIONING is
  provided for testing the funny former behavior.

  The new rule implies the following behavior.
  - Setting commands (`Dt', `Df', `DF') and polygons (`Dp' and `DP')
    do not change position now.
  - Filled circles and ellipses (`DC' and `DE') position at their
    most right point (outlined ones `Dc' and `De' did this anyway).
  - As before, all open graphical objects position to their final
    drawing point (alternate sum of the command arguments).

*/

#ifndef STUPID_DRAWING_POSITIONING
// uncomment next line if all non-classical D commands shall position
// to the strange alternate sum of args displacement
#define STUPID_DRAWING_POSITIONING
#endif

// Decide whether the commands `{' and `}' for different environments
// should be used.
#undef USE_ENV_STACK

#include "driver.h"
#include "device.h"

#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
#include <math.h>


/**********************************************************************
                           local types
 **********************************************************************/

// integer type used in the fields of struct environment (see printer.h)
typedef int EnvInt;

// integer arguments of groff_out commands, must be >= 32 bits
typedef int IntArg;

// color components of groff_out color commands, must be >= 32 bits
typedef unsigned int ColorArg;

// Array for IntArg values.
class IntArray {
  size_t num_allocated;
  size_t num_stored;
  IntArg *data;
public:
  IntArray(void);
  IntArray(const size_t);
  ~IntArray(void);
  IntArg operator[](const size_t i) const
  {
    if (i >= num_stored)
      fatal("index out of range");
    return (IntArg) data[i];
  }
  void append(IntArg);
  IntArg *get_data(void) const { return (IntArg *)data; }
  size_t len(void) const { return num_stored; }
};

// Characters read from the input queue.
class Char {
  int data;
public:
  Char(void) : data('\0') {}
  Char(const int c) : data(c) {}
  bool operator==(char c) const { return (data == c) ? true : false; }
  bool operator==(int c) const { return (data == c) ? true : false; }
  bool operator==(const Char c) const
		  { return (data == c.data) ? true : false; }
  bool operator!=(char c) const { return !(*this == c); }
  bool operator!=(int c) const { return !(*this == c); }
  bool operator!=(const Char c) const { return !(*this == c); }
  operator int() const { return (int) data; }
  operator unsigned char() const { return (unsigned char) data; }
  operator char() const { return (char) data; }
};

// Buffer for string arguments (Char, not char).
class StringBuf {
  size_t num_allocated;
  size_t num_stored;
  Char *data;			// not terminated by '\0'
public:
  StringBuf(void);		// allocate without storing
  ~StringBuf(void);
  void append(const Char);	// append character to `data'
  char *make_string(void);	// return new copy of `data' with '\0'
  bool is_empty(void) {		// true if none stored
    return (num_stored > 0) ? false : true;
  }
  void reset(void);		// set `num_stored' to 0
};

#ifdef USE_ENV_STACK
class EnvStack {
  environment **data;
  size_t num_allocated;
  size_t num_stored;
public:
  EnvStack(void);
  ~EnvStack(void);
  environment *pop(void);
  void push(environment *e);
};
#endif // USE_ENV_STACK


/**********************************************************************
                          external variables
 **********************************************************************/

// exported as extern by error.h (called from driver.h)
// needed for error messages (see ../libgroff/error.cpp)
const char *current_filename = 0; // printable name of the current file
				  // printable name of current source file
const char *current_source_filename = 0;
int current_lineno = 0;		  // current line number of printout

// exported as extern by device.h;
const char *device = 0;		  // cancel former init with literal

printer *pr;

// Note:
//
//   We rely on an implementation of the `new' operator which aborts
//   gracefully if it can't allocate memory (e.g. from libgroff/new.cpp).


/**********************************************************************
                        static local variables
 **********************************************************************/

FILE *current_file = 0;		// current input stream for parser

// npages: number of pages processed so far (including current page),
//         _not_ the page number in the printout (can be set with `p').
int npages = 0;

const ColorArg
COLORARG_MAX = (ColorArg) 65536U; // == 0xFFFF + 1 == 0x10000

const IntArg
INTARG_MAX = (IntArg) 0x7FFFFFFF; // maximal signed 32 bits number

// parser environment, created and deleted by each run of do_file()
environment *current_env = 0;

#ifdef USE_ENV_STACK
const size_t
envp_size = sizeof(environment *);
#endif // USE_ENV_STACK


/**********************************************************************
                        function declarations
 **********************************************************************/

// utility functions
ColorArg color_from_Df_command(IntArg);
				// transform old color into new
void delete_current_env(void);	// delete global var current_env
void fatal_command(char);	// abort for invalid command
inline Char get_char(void);	// read next character from input stream
ColorArg get_color_arg(void);	// read in argument for new color cmds
IntArray *get_D_fixed_args(const size_t);
				// read in fixed number of integer
				// arguments
IntArray *get_D_fixed_args_odd_dummy(const size_t);
				// read in a fixed number of integer
				// arguments plus optional dummy
IntArray *get_D_variable_args(void);
                                // variable, even number of int args
char *get_extended_arg(void);	// argument for `x X' (several lines)
IntArg get_integer_arg(void);	// read in next integer argument
IntArray *get_possibly_integer_args();
				// 0 or more integer arguments
char *get_string_arg(void);	// read in next string arg, ended by WS
inline bool is_space_or_tab(const Char);
				// test on space/tab char
Char next_arg_begin(void);	// skip white space on current line
Char next_command(void);	// go to next command, evt. diff. line
inline bool odd(const int);	// test if integer is odd
void position_to_end_of_args(const IntArray * const);
				// positioning after drawing
void remember_filename(const char *);
				// set global current_filename
void remember_source_filename(const char *);
				// set global current_source_filename
void send_draw(const Char, const IntArray * const);
				// call pr->draw
void skip_line(void);		// unconditionally skip to next line
bool skip_line_checked(void);	// skip line, false if args are left
void skip_line_fatal(void);	// skip line, fatal if args are left
void skip_line_warn(void);	// skip line, warn if args are left
void skip_line_D(void);		// skip line in D commands
void skip_line_x(void);		// skip line in x commands
void skip_to_end_of_line(void);	// skip to the end of the current line
inline void unget_char(const Char);
				// restore character onto input

// parser subcommands
void parse_color_command(color *);
				// color sub(sub)commands m and DF
void parse_D_command(void);	// graphical subcommands
bool parse_x_command(void);	// device controller subcommands


/**********************************************************************
                         class methods
 **********************************************************************/

#ifdef USE_ENV_STACK
EnvStack::EnvStack(void)
{
  num_allocated = 4;
  // allocate pointer to array of num_allocated pointers to environment
  data = (environment **)malloc(envp_size * num_allocated);
  if (data == 0)
    fatal("could not allocate environment data");
  num_stored = 0;
}

EnvStack::~EnvStack(void)
{
  for (size_t i = 0; i < num_stored; i++)
    delete data[i];
  free(data);
}

// return top element from stack and decrease stack pointer
//
// the calling function must take care of properly deleting the result
environment *
EnvStack::pop(void)
{
  num_stored--;
  environment *result = data[num_stored];
  data[num_stored] = 0;
  return result;
}

// copy argument and push this onto the stack
void
EnvStack::push(environment *e)
{
  environment *e_copy = new environment;
  if (num_stored >= num_allocated) {
    environment **old_data = data;
    num_allocated *= 2;
    data = (environment **)malloc(envp_size * num_allocated);
    if (data == 0)
      fatal("could not allocate data");
    for (size_t i = 0; i < num_stored; i++)
      data[i] = old_data[i];
    free(old_data);
  }
  e_copy->col = new color;
  e_copy->fill = new color;
  *e_copy->col = *e->col;
  *e_copy->fill = *e->fill;
  e_copy->fontno = e->fontno;
  e_copy->height = e->height;
  e_copy->hpos = e->hpos;
  e_copy->size = e->size;
  e_copy->slant = e->slant;
  e_copy->vpos = e->vpos;
  data[num_stored] = e_copy;
  num_stored++;
}
#endif // USE_ENV_STACK

IntArray::IntArray(void)
{
  num_allocated = 4;
  data = new IntArg[num_allocated];
  num_stored = 0;
}

IntArray::IntArray(const size_t n)
{
  if (n <= 0)
    fatal("number of integers to be allocated must be > 0");
  num_allocated = n;
  data = new IntArg[num_allocated];
  num_stored = 0;
}

IntArray::~IntArray(void)
{
  a_delete data;
}

void
IntArray::append(IntArg x)
{
  if (num_stored >= num_allocated) {
    IntArg *old_data = data;
    num_allocated *= 2;
    data = new IntArg[num_allocated];
    for (size_t i = 0; i < num_stored; i++)
      data[i] = old_data[i];
    a_delete old_data;
  }
  data[num_stored] = x;
  num_stored++;
}

StringBuf::StringBuf(void)
{
  num_stored = 0;
  num_allocated = 128;
  data = new Char[num_allocated];
}

StringBuf::~StringBuf(void)
{
  a_delete data;
}

void
StringBuf::append(const Char c)
{
  if (num_stored >= num_allocated) {
    Char *old_data = data;
    num_allocated *= 2;
    data = new Char[num_allocated];
    for (size_t i = 0; i < num_stored; i++)
      data[i] = old_data[i];
    a_delete old_data;
  }
  data[num_stored] = c;
  num_stored++;
}

char *
StringBuf::make_string(void)
{
  char *result = new char[num_stored + 1];
  for (size_t i = 0; i < num_stored; i++)
    result[i] = (char) data[i];
  result[num_stored] = '\0';
  return result;
}

void
StringBuf::reset(void)
{
  num_stored = 0;
}

/**********************************************************************
                        utility functions
 **********************************************************************/

//////////////////////////////////////////////////////////////////////
/* color_from_Df_command:
   Process the gray shade setting command Df.

   Transform Df style color into DF style color.
   Df color: 0-1000, 0 is white
   DF color: 0-65536, 0 is black

   The Df command is obsoleted by command DFg, but kept for
   compatibility.
*/
ColorArg
color_from_Df_command(IntArg Df_gray)
{
  return ColorArg((1000-Df_gray) * COLORARG_MAX / 1000); // scaling
}

//////////////////////////////////////////////////////////////////////
/* delete_current_env():
   Delete global variable current_env and its pointer members.

   This should be a class method of environment.
*/
void delete_current_env(void)
{
  delete current_env->col;
  delete current_env->fill;
  delete current_env;
  current_env = 0;
}

//////////////////////////////////////////////////////////////////////
/* fatal_command():
   Emit error message about invalid command and abort.
*/
void
fatal_command(char command)
{
  fatal("`%1' command invalid before first `p' command", command);
}

//////////////////////////////////////////////////////////////////////
/* get_char():
   Retrieve the next character from the input queue.

   Return: The retrieved character (incl. EOF), converted to Char.
*/
inline Char
get_char(void)
{
  return (Char) getc(current_file);
}

//////////////////////////////////////////////////////////////////////
/* get_color_arg():
   Retrieve an argument suitable for the color commands m and DF.

   Return: The retrieved color argument.
*/
ColorArg
get_color_arg(void)
{
  IntArg x = get_integer_arg();
  if (x < 0 || x > (IntArg)COLORARG_MAX) {
    error("color component argument out of range");
    x = 0;
  }
  return (ColorArg) x;
}

//////////////////////////////////////////////////////////////////////
/* get_D_fixed_args():
   Get a fixed number of integer arguments for D commands.

   Fatal if wrong number of arguments.
   Too many arguments on the line raise a warning.
   A line skip is done.

   number: In-parameter, the number of arguments to be retrieved.
   ignore: In-parameter, ignore next argument -- GNU troff always emits
           pairs of parameters for `D' extensions added by groff.
           Default is `false'.

   Return: New IntArray containing the arguments.
*/
IntArray *
get_D_fixed_args(const size_t number)
{
  if (number <= 0)
    fatal("requested number of arguments must be > 0");
  IntArray *args = new IntArray(number);
  for (size_t i = 0; i < number; i++)
    args->append(get_integer_arg());
  skip_line_D();
  return args;
}

//////////////////////////////////////////////////////////////////////
/* get_D_fixed_args_odd_dummy():
   Get a fixed number of integer arguments for D commands and optionally
   ignore a dummy integer argument if the requested number is odd.

   The gtroff program adds a dummy argument to some commands to get
   an even number of arguments.
   Error if the number of arguments differs from the scheme above.
   A line skip is done.

   number: In-parameter, the number of arguments to be retrieved.

   Return: New IntArray containing the arguments.
*/
IntArray *
get_D_fixed_args_odd_dummy(const size_t number)
{
  if (number <= 0)
    fatal("requested number of arguments must be > 0");
  IntArray *args = new IntArray(number);
  for (size_t i = 0; i < number; i++)
    args->append(get_integer_arg());
  if (odd(number)) {
    IntArray *a = get_possibly_integer_args();
    if (a->len() > 1)
      error("too many arguments");
    delete a;
  }
  skip_line_D();
  return args;
}

//////////////////////////////////////////////////////////////////////
/* get_D_variable_args():
   Get a variable even number of integer arguments for D commands.

   Get as many integer arguments as possible from the rest of the
   current line.
   - The arguments are separated by an arbitrary sequence of space or
     tab characters.
   - A comment, a newline, or EOF indicates the end of processing.
   - Error on non-digit characters different from these.
   - A final line skip is performed (except for EOF).

   Return: New IntArray of the retrieved arguments.
*/
IntArray *
get_D_variable_args()
{
  IntArray *args = get_possibly_integer_args();
  size_t n = args->len();
  if (n <= 0)
    error("no arguments found");
  if (odd(n))
    error("even number of arguments expected");
  skip_line_D();
  return args;
}

//////////////////////////////////////////////////////////////////////
/* get_extended_arg():
   Retrieve extended arg for `x X' command.

   - Skip leading spaces and tabs, error on EOL or newline.
   - Return everything before the next NL or EOF ('#' is not a comment);
     as long as the following line starts with '+' this is returned
     as well, with the '+' replaced by a newline.
   - Final line skip is always performed.

   Return: Allocated (new) string of retrieved text argument.
*/
char *
get_extended_arg(void)
{
  StringBuf buf = StringBuf();
  Char c = next_arg_begin();
  while ((int) c != EOF) {
    if ((int) c == '\n') {
      current_lineno++;
      c = get_char();
      if ((int) c == '+')
	buf.append((Char) '\n');
      else {
	unget_char(c);		// first character of next line
	break;
      }
    }
    else
      buf.append(c);
    c = get_char();
  }
  return buf.make_string();
}

//////////////////////////////////////////////////////////////////////
/* get_integer_arg(): Retrieve integer argument.

   Skip leading spaces and tabs, collect an optional '-' and all
   following decimal digits (at least one) up to the next non-digit,
   which is restored onto the input queue.

   Fatal error on all other situations.

   Return: Retrieved integer.
*/
IntArg
get_integer_arg(void)
{
  StringBuf buf = StringBuf();
  Char c = next_arg_begin();
  if ((int) c == '-') {
    buf.append(c);
    c = get_char();
  }
  if (!isdigit((int) c))
    error("integer argument expected");
  while (isdigit((int) c)) {
    buf.append(c);
    c = get_char();
  }
  // c is not a digit
  unget_char(c);
  char *s = buf.make_string();
  errno = 0;
  long int number = strtol(s, 0, 10);
  if (errno != 0
      || number > INTARG_MAX || number < -INTARG_MAX) {
    error("integer argument too large");
    number = 0;
  }
  a_delete s;
  return (IntArg) number;
}

//////////////////////////////////////////////////////////////////////
/* get_possibly_integer_args():
   Parse the rest of the input line as a list of integer arguments.

   Get as many integer arguments as possible from the rest of the
   current line, even none.
   - The arguments are separated by an arbitrary sequence of space or
     tab characters.
   - A comment, a newline, or EOF indicates the end of processing.
   - Error on non-digit characters different from these.
   - No line skip is performed.

   Return: New IntArray of the retrieved arguments.
*/
IntArray *
get_possibly_integer_args()
{
  bool done = false;
  StringBuf buf = StringBuf();
  Char c = get_char();
  IntArray *args = new IntArray();
  while (!done) {
    buf.reset();
    while (is_space_or_tab(c))
      c = get_char();
    if (c == '-') {
      Char c1 = get_char();
      if (isdigit((int) c1)) {
	buf.append(c);
	c = c1;
      }
      else
	unget_char(c1);
    }
    while (isdigit((int) c)) {
      buf.append(c);
      c = get_char();
    }
    if (!buf.is_empty()) {
      char *s = buf.make_string();
      errno = 0;
      long int x = strtol(s, 0, 10);
      if (errno
	  || x > INTARG_MAX || x < -INTARG_MAX) {
	error("invalid integer argument, set to 0");
	x = 0;
      }
      args->append((IntArg) x);
      a_delete s;
    }
    // Here, c is not a digit.
    // Terminate on comment, end of line, or end of file, while
    // space or tab indicate continuation; otherwise error.
    switch((int) c) {
    case '#':
      skip_to_end_of_line();
      done = true;
      break;
    case '\n':
      done = true;
      unget_char(c);
      break;
    case EOF:
      done = true;
      break;
    case ' ':
    case '\t':
      break;
    default:
      error("integer argument expected");
      break;
    }
  }
  return args;
}

//////////////////////////////////////////////////////////////////////
/* get_string_arg():
   Retrieve string arg.

   - Skip leading spaces and tabs; error on EOL or newline.
   - Return all following characters before the next space, tab,
     newline, or EOF character (in-word '#' is not a comment character).
   - The terminating space, tab, newline, or EOF character is restored
     onto the input queue, so no line skip.

   Return: Retrieved string as char *, allocated by 'new'.
*/
char *
get_string_arg(void)
{
  StringBuf buf = StringBuf();
  Char c = next_arg_begin();
  while (!is_space_or_tab(c)
	 && c != Char('\n') && c != Char(EOF)) {
    buf.append(c);
    c = get_char();
  }
  unget_char(c);		// restore white space
  return buf.make_string();
}

//////////////////////////////////////////////////////////////////////
/* is_space_or_tab():
   Test a character if it is a space or tab.

   c: In-parameter, character to be tested.

   Return: True, if c is a space or tab character, false otherwise.
*/
inline bool
is_space_or_tab(const Char c)
{
  return (c == Char(' ') || c == Char('\t')) ? true : false;
}

//////////////////////////////////////////////////////////////////////
/* next_arg_begin():
   Return first character of next argument.

   Skip space and tab characters; error on newline or EOF.

   Return: The first character different from these (including '#').
*/
Char
next_arg_begin(void)
{
  Char c;
  while (1) {
    c = get_char();
    switch ((int) c) {
    case ' ':
    case '\t':
      break;
    case '\n':
    case EOF:
      error("missing argument");
      break;
    default:			// first essential character
      return c;
    }
  }
}

//////////////////////////////////////////////////////////////////////
/* next_command():
   Find the first character of the next command.

   Skip spaces, tabs, comments (introduced by #), and newlines.

   Return: The first character different from these (including EOF).
*/
Char
next_command(void)
{
  Char c;
  while (1) {
    c = get_char();
    switch ((int) c) {
    case ' ':
    case '\t':
      break;
    case '\n':
      current_lineno++;
      break;
    case '#':			// comment
      skip_line();
      break;
    default:			// EOF or first essential character
      return c;
    }
  }
}

//////////////////////////////////////////////////////////////////////
/* odd():
   Test whether argument is an odd number.

   n: In-parameter, the integer to be tested.

   Return: True if odd, false otherwise.
*/
inline bool
odd(const int n)
{
  return (n & 1 == 1) ? true : false;
}

//////////////////////////////////////////////////////////////////////
/* position_to_end_of_args():
   Move graphical pointer to end of drawn figure.

   This is used by the D commands that draw open geometrical figures.
   The algorithm simply sums up all horizontal displacements (arguments
   with even number) for the horizontal component.  Similarly, the
   vertical component is the sum of the odd arguments.

   args: In-parameter, the arguments of a former drawing command.
*/
void
position_to_end_of_args(const IntArray * const args)
{
  size_t i;
  const size_t n = args->len();
  for (i = 0; i < n; i += 2)
    current_env->hpos += (*args)[i];
  for (i = 1; i < n; i += 2)
    current_env->vpos += (*args)[i];
}

//////////////////////////////////////////////////////////////////////
/* remember_filename():
   Set global variable current_filename.

   The actual filename is stored in current_filename.  This is used by
   the postprocessors, expecting the name "<standard input>" for stdin.

   filename: In-out-parameter; is changed to the new value also.
*/
void
remember_filename(const char *filename)
{
  char *fname;
  if (strcmp(filename, "-") == 0)
    fname = (char *)"<standard input>";
  else
    fname = (char *)filename;
  size_t len = strlen(fname) + 1;
  if (current_filename != 0)
    free((char *)current_filename);
  current_filename = (const char *)malloc(len);
  if (current_filename == 0)
    fatal("can't malloc space for filename");
  strncpy((char *)current_filename, (char *)fname, len);
}

//////////////////////////////////////////////////////////////////////
/* remember_source_filename():
   Set global variable current_source_filename.

   The actual filename is stored in current_filename.  This is used by
   the postprocessors, expecting the name "<standard input>" for stdin.

   filename: In-out-parameter; is changed to the new value also.
*/
void
remember_source_filename(const char *filename)
{
  char *fname;
  if (strcmp(filename, "-") == 0)
    fname = (char *)"<standard input>";
  else
    fname = (char *)filename;
  size_t len = strlen(fname) + 1;
  if (current_source_filename != 0)
    free((char *)current_source_filename);
  current_source_filename = (const char *)malloc(len);
  if (current_source_filename == 0)
    fatal("can't malloc space for filename");
  strncpy((char *)current_source_filename, (char *)fname, len);
}

//////////////////////////////////////////////////////////////////////
/* send_draw():
   Call draw method of printer class.

   subcmd: Letter of actual D subcommand.
   args: Array of integer arguments of actual D subcommand.
*/
void
send_draw(const Char subcmd, const IntArray * const args)
{
  EnvInt n = (EnvInt) args->len();
  pr->draw((int) subcmd, (IntArg *)args->get_data(), n, current_env);
}

//////////////////////////////////////////////////////////////////////
/* skip_line():
   Go to next line within the input queue.

   Skip the rest of the current line, including the newline character.
   The global variable current_lineno is adjusted.
   No errors are raised.
*/
void
skip_line(void)
{
  Char c = get_char();
  while (1) {
    if (c == '\n') {
      current_lineno++;
      break;
    }
    if (c == EOF)
      break;
    c = get_char();
  }
}

//////////////////////////////////////////////////////////////////////
/* skip_line_checked ():
   Check that there aren't any arguments left on the rest of the line,
   then skip line.

   Spaces, tabs, and a comment are allowed before newline or EOF.
   All other characters raise an error.
*/
bool
skip_line_checked(void)
{
  bool ok = true;
  Char c = get_char();
  while (is_space_or_tab(c))
    c = get_char();
  switch((int) c) {
  case '#':			// comment
    skip_line();
    break;
  case '\n':
    current_lineno++;
    break;
  case EOF:
    break;
  default:
    ok = false;
    skip_line();
    break;
  }
  return ok;
}

//////////////////////////////////////////////////////////////////////
/* skip_line_fatal ():
   Fatal error if arguments left, otherwise skip line.

   Spaces, tabs, and a comment are allowed before newline or EOF.
   All other characters trigger the error.
*/
void
skip_line_fatal(void)
{
  bool ok = skip_line_checked();
  if (!ok) {
    current_lineno--;
    error("too many arguments");
    current_lineno++;
  }
}

//////////////////////////////////////////////////////////////////////
/* skip_line_warn ():
   Skip line, but warn if arguments are left on actual line.

   Spaces, tabs, and a comment are allowed before newline or EOF.
   All other characters raise a warning
*/
void
skip_line_warn(void)
{
  bool ok = skip_line_checked();
  if (!ok) {
    current_lineno--;
    warning("too many arguments on current line");
    current_lineno++;
  }
}

//////////////////////////////////////////////////////////////////////
/* skip_line_D ():
   Skip line in `D' commands.

   Decide whether in case of an additional argument a fatal error is
   raised (the documented classical behavior), only a warning is
   issued, or the line is just skipped (former groff behavior).
   Actually decided for the warning.
*/
void
skip_line_D(void)
{
  skip_line_warn();
  // or: skip_line_fatal();
  // or: skip_line();
}

//////////////////////////////////////////////////////////////////////
/* skip_line_x ():
   Skip line in `x' commands.

   Decide whether in case of an additional argument a fatal error is
   raised (the documented classical behavior), only a warning is
   issued, or the line is just skipped (former groff behavior).
   Actually decided for the warning.
*/
void
skip_line_x(void)
{
  skip_line_warn();
  // or: skip_line_fatal();
  // or: skip_line();
}

//////////////////////////////////////////////////////////////////////
/* skip_to_end_of_line():
   Go to the end of the current line.

   Skip the rest of the current line, excluding the newline character.
   The global variable current_lineno is not changed.
   No errors are raised.
*/
void
skip_to_end_of_line(void)
{
  Char c = get_char();
  while (1) {
    if (c == '\n') {
      unget_char(c);
      return;
    }
    if (c == EOF)
      return;
    c = get_char();
  }
}

//////////////////////////////////////////////////////////////////////
/* unget_char(c):
   Restore character c onto input queue.

   Write a character back onto the input stream.
   EOF is gracefully handled.

   c: In-parameter; character to be pushed onto the input queue.
*/
inline void
unget_char(const Char c)
{
  if (c != EOF) {
    int ch = (int) c;
    if (ungetc(ch, current_file) == EOF)
      fatal("could not unget character");
  }
}


/**********************************************************************
                       parser subcommands
 **********************************************************************/

//////////////////////////////////////////////////////////////////////
/* parse_color_command:
   Process the commands m and DF, but not Df.

   col: In-out-parameter; the color object to be set, must have
        been initialized before.
*/
void
parse_color_command(color *col)
{
  ColorArg gray = 0;
  ColorArg red = 0, green = 0, blue = 0;
  ColorArg cyan = 0, magenta = 0, yellow = 0, black = 0;
  Char subcmd = next_arg_begin();
  switch((int) subcmd) {
  case 'c':			// DFc or mc: CMY
    cyan = get_color_arg();
    magenta = get_color_arg();
    yellow = get_color_arg();
    col->set_cmy(cyan, magenta, yellow);
    break;
  case 'd':			// DFd or md: set default color
    col->set_default();
    break;
  case 'g':			// DFg or mg: gray
    gray = get_color_arg();
    col->set_gray(gray);
    break;
  case 'k':			// DFk or mk: CMYK
    cyan = get_color_arg();
    magenta = get_color_arg();
    yellow = get_color_arg();
    black = get_color_arg();
    col->set_cmyk(cyan, magenta, yellow, black);
    break;
  case 'r':			// DFr or mr: RGB
    red = get_color_arg();
    green = get_color_arg();
    blue = get_color_arg();
    col->set_rgb(red, green, blue);
    break;
  default:
    error("invalid color scheme `%1'", (int) subcmd);
    break;
  } // end of color subcommands
}

//////////////////////////////////////////////////////////////////////
/* parse_D_command():
   Parse the subcommands of graphical command D.

   This is the part of the do_file() parser that scans the graphical
   subcommands.
   - Error on lacking or wrong arguments.
   - Warning on too many arguments.
   - Line is always skipped.
*/
void
parse_D_command()
{
  Char subcmd = next_arg_begin();
  switch((int) subcmd) {
  case '~':			// D~: draw B-spline
    // actually, this isn't available for some postprocessors
    // fall through
  default:			// unknown options are passed to device
    {
      IntArray *args = get_D_variable_args();
      send_draw(subcmd, args);
      position_to_end_of_args(args);
      delete args;
      break;
    }
  case 'a':			// Da: draw arc
    {
      IntArray *args = get_D_fixed_args(4);
      send_draw(subcmd, args);
      position_to_end_of_args(args);
      delete args;
      break;
    }
  case 'c':			// Dc: draw circle line
    {
      IntArray *args = get_D_fixed_args(1);
      send_draw(subcmd, args);
      // move to right end
      current_env->hpos += (*args)[0];
      delete args;
      break;
    }
  case 'C':			// DC: draw solid circle
    {
      IntArray *args = get_D_fixed_args_odd_dummy(1);
      send_draw(subcmd, args);
      // move to right end
      current_env->hpos += (*args)[0];
      delete args;
      break;
    }
  case 'e':			// De: draw ellipse line
  case 'E':			// DE: draw solid ellipse
    {
      IntArray *args = get_D_fixed_args(2);
      send_draw(subcmd, args);
      // move to right end
      current_env->hpos += (*args)[0];
      delete args;
      break;
    }
  case 'f':			// Df: set fill gray; obsoleted by DFg
    {
      IntArg arg = get_integer_arg();
      if ((arg >= 0) && (arg <= 1000)) {
	// convert arg and treat it like DFg
	ColorArg gray = color_from_Df_command(arg);
        current_env->fill->set_gray(gray);
      }
      else {
	// set fill color to the same value as the current outline color
	delete current_env->fill;
	current_env->fill = new color(current_env->col);
      }
      pr->change_fill_color(current_env);
      // skip unused `vertical' component (\D'...' always emits pairs)
      (void) get_integer_arg();
#   ifdef STUPID_DRAWING_POSITIONING
      current_env->hpos += arg;
#   endif
      skip_line_x();
      break;
    }
  case 'F':			// DF: set fill color, several formats
    parse_color_command(current_env->fill);
    pr->change_fill_color(current_env);
    // no positioning (setting-only command)
    skip_line_x();
    break;
  case 'l':			// Dl: draw line
    {
      IntArray *args = get_D_fixed_args(2);
      send_draw(subcmd, args);
      position_to_end_of_args(args);
      delete args;
      break;
    }
  case 'p':			// Dp: draw closed polygon line
  case 'P':			// DP: draw solid closed polygon
    {
      IntArray *args = get_D_variable_args();
      send_draw(subcmd, args);
#   ifdef STUPID_DRAWING_POSITIONING
      // final args positioning
      position_to_end_of_args(args);
#   endif
      delete args;
      break;
    }
  case 't':			// Dt: set line thickness
    {
      IntArray *args = get_D_fixed_args_odd_dummy(1);
      send_draw(subcmd, args);
#   ifdef STUPID_DRAWING_POSITIONING
      // final args positioning
      position_to_end_of_args(args);
#   endif
      delete args;
      break;
    }
  } // end of D subcommands
}

//////////////////////////////////////////////////////////////////////
/* parse_x_command():
   Parse subcommands of the device control command x.

   This is the part of the do_file() parser that scans the device
   controlling commands.
   - Error on duplicate prologue commands.
   - Error on wrong or lacking arguments.
   - Warning on too many arguments.
   - Line is always skipped.

   Globals:
   - current_env: is set by many subcommands.
   - npages: page counting variable

   Return: boolean in the meaning of `stopped'
           - true if parsing should be stopped (`x stop').
           - false if parsing should continue.
*/
bool
parse_x_command(void)
{
  bool stopped = false;
  char *subcmd_str = get_string_arg();
  char subcmd = subcmd_str[0];
  switch (subcmd) {
  case 'f':			// x font: mount font
    {
      IntArg n = get_integer_arg();
      char *name = get_string_arg();
      pr->load_font(n, name);
      a_delete name;
      skip_line_x();
      break;
    }
  case 'F':			// x Filename: set filename for errors
    {
      char *str_arg = get_string_arg();
      if (str_arg == 0)
	warning("empty argument for `x F' command");
      else {
	remember_source_filename(str_arg);
	a_delete str_arg;
      }
      break;
    }
  case 'H':			// x Height: set character height
    current_env->height = get_integer_arg();
    if (current_env->height == current_env->size)
      current_env->height = 0;
    skip_line_x();
    break;
  case 'i':			// x init: initialize device
    error("duplicate `x init' command");
    skip_line_x();
    break;
  case 'p':			// x pause: pause device
    skip_line_x();
    break;
  case 'r':			// x res: set resolution
    error("duplicate `x res' command");
    skip_line_x();
    break;
  case 's':			// x stop: stop device
    stopped = true;
    skip_line_x();
    break;
  case 'S':			// x Slant: set slant
    current_env->slant = get_integer_arg();
    skip_line_x();
    break;
  case 't':			// x trailer: generate trailer info
    skip_line_x();
    break;
  case 'T':			// x Typesetter: set typesetter
    error("duplicate `x T' command");
    skip_line();
    break;
  case 'u':			// x underline: from .cu
    {
      char *str_arg = get_string_arg();
      pr->special(str_arg, current_env, 'u');
      a_delete str_arg;
      skip_line_x();
      break;
    }
  case 'X':			// x X: send uninterpretedly to device
    {
      char *str_arg = get_extended_arg(); // includes line skip
      if (npages <= 0)
	error("`x X' command invalid before first `p' command");
      else if (str_arg && (strncmp(str_arg, "devtag:",
				   strlen("devtag:")) == 0))
	pr->devtag(str_arg, current_env);
      else
	pr->special(str_arg, current_env);
      a_delete str_arg;
      break;
    }
  default:			// ignore unknown x commands, but warn
    warning("unknown command `x %1'", subcmd);
    skip_line();
  }
  a_delete subcmd_str;
  return stopped;
}


/**********************************************************************
                     exported part (by driver.h)
 **********************************************************************/

////////////////////////////////////////////////////////////////////////
/* do_file():
   Parse and postprocess groff intermediate output.

   filename: "-" for standard input, normal file name otherwise
*/
void
do_file(const char *filename)
{
  Char command;
  bool stopped = false;		// terminating condition

#ifdef USE_ENV_STACK
  EnvStack env_stack = EnvStack();
#endif // USE_ENV_STACK

  // setup of global variables
  npages = 0;
  current_lineno = 1;
  // `pr' is initialized after the prologue.
  // `device' is set by the 1st prologue command.

  if (filename[0] == '-' && filename[1] == '\0')
    current_file = stdin;
  else {
    errno = 0;
    current_file = fopen(filename, "r");
    if (errno != 0 || current_file == 0) {
      error("can't open file `%1'", filename);
      return;
    }
  }
  remember_filename(filename);

  if (current_env != 0)
    delete_current_env();
  current_env = new environment;
  current_env->col = new color;
  current_env->fill = new color;
  current_env->fontno = -1;
  current_env->height = 0;
  current_env->hpos = -1;
  current_env->slant = 0;
  current_env->size = 0;
  current_env->vpos = -1;

  // parsing of prologue (first 3 commands)
  {
    char *str_arg;
    IntArg int_arg;

    // 1st command `x T'
    command = next_command();	
    if ((int) command == EOF)
      return;
    if ((int) command != 'x')
      fatal("the first command must be `x T'");
    str_arg = get_string_arg();
    if (str_arg[0] != 'T')
      fatal("the first command must be `x T'");
    a_delete str_arg;
    char *tmp_dev = get_string_arg();
    if (pr == 0) {		// note: `pr' initialized after prologue
      device = tmp_dev;
      if (!font::load_desc())
	fatal("couldn't load DESC file, can't continue");
    }
    else {
      if (device == 0 || strcmp(device, tmp_dev) != 0)
	fatal("all files must use the same device");
      a_delete tmp_dev;
    }
    skip_line_x();		// ignore further arguments
    current_env->size = 10 * font::sizescale;

    // 2nd command `x res'
    command = next_command();
    if ((int) command != 'x')
      fatal("the second command must be `x res'");
    str_arg = get_string_arg();
    if (str_arg[0] != 'r')
      fatal("the second command must be `x res'");
    a_delete str_arg;
    int_arg = get_integer_arg();
    EnvInt font_res = font::res;
    if (int_arg != font_res)
      fatal("resolution does not match");
    int_arg = get_integer_arg();
    if (int_arg != font::hor)
      fatal("minimum horizontal motion does not match");
    int_arg = get_integer_arg();
    if (int_arg != font::vert)
      fatal("minimum vertical motion does not match");
    skip_line_x();		// ignore further arguments

    // 3rd command `x init'
    command = next_command();
    if (command != 'x')
      fatal("the third command must be `x init'");
    str_arg = get_string_arg();
    if (str_arg[0] != 'i')
      fatal("the third command must be `x init'");
    a_delete str_arg;
    skip_line_x();
  }

  // parsing of body
  if (pr == 0)
    pr = make_printer();
  while (!stopped) {
    command = next_command();
    if (command == EOF)
      break;
    // spaces, tabs, comments, and newlines are skipped here
    switch ((int) command) {
    case '#':			// #: comment, ignore up to end of line
      skip_line();
      break;
#ifdef USE_ENV_STACK
    case '{':			// {: start a new environment (a copy)
      env_stack.push(current_env);
      break;
    case '}':			// }: pop previous env from stack
      delete_current_env();
      current_env = env_stack.pop();
      break;
#endif // USE_ENV_STACK
    case '0':			// ddc: obsolete jump and print command
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
      {				// expect 2 digits and a character
	char s[3];
	Char c = next_arg_begin();
	if (npages <= 0)
	  fatal_command(command);
	if (!isdigit((int) c)) {
	  error("digit expected");
	  c = 0;
	}
	s[0] = (char) command;
	s[1] = (char) c;
	s[2] = '\0';
	errno = 0;
	long int x = strtol(s, 0, 10);
	if (errno != 0)
	  error("couldn't convert 2 digits");
	EnvInt hor_pos = (EnvInt) x;
	current_env->hpos += hor_pos;
	c = next_arg_begin();
	if ((int) c == '\n' || (int) c == EOF)
	  error("character argument expected");
	else
	  pr->set_ascii_char((unsigned char) c, current_env);
	break;
      }
    case 'c':			// c: print ascii char without moving
      {
	if (npages <= 0)
	  fatal_command(command);
	Char c = next_arg_begin();
	if (c == '\n' || c == EOF)
	  error("missing argument to `c' command");
	else
	  pr->set_ascii_char((unsigned char) c, current_env);
	break;
      }
    case 'C':			// C: print named special character
      {
	if (npages <= 0)
	  fatal_command(command);
	char *str_arg = get_string_arg();
	pr->set_special_char(str_arg, current_env);
	a_delete str_arg;
	break;
      }
    case 'D':			// drawing commands
      if (npages <= 0)
	fatal_command(command);
      parse_D_command();
      break;
    case 'f':			// f: set font to number
      current_env->fontno = get_integer_arg();
      break;
    case 'F':			// F: obsolete, replaced by `x F'
      {
	char *str_arg = get_string_arg();
	remember_source_filename(str_arg);
	a_delete str_arg;
	break;
      }
    case 'h':			// h: relative horizontal move
      current_env->hpos += (EnvInt) get_integer_arg();
      break;
    case 'H':			// H: absolute horizontal positioning
      current_env->hpos = (EnvInt) get_integer_arg();
      break;
    case 'm':			// m: glyph color
      parse_color_command(current_env->col);
      pr->change_color(current_env);
      break;
    case 'n':			// n: print end of line
				// ignore two arguments (historically)
      if (npages <= 0)
	fatal_command(command);
      pr->end_of_line();
      (void) get_integer_arg();
      (void) get_integer_arg();
      break;
    case 'N':			// N: print char with given int code
      if (npages <= 0)
	fatal_command(command);
      pr->set_numbered_char(get_integer_arg(), current_env);
      break;
    case 'p':			// p: start new page with given number
      if (npages > 0)
	pr->end_page(current_env->vpos);
      npages++;			// increment # of processed pages
      pr->begin_page(get_integer_arg());
      current_env->vpos = 0;
      break;
    case 's':			// s: set point size
      current_env->size = get_integer_arg();
      if (current_env->height == current_env->size)
	current_env->height = 0;
      break;
    case 't':			// t: print a text word
      {
	char c;
	if (npages <= 0)
	  fatal_command(command);
	char *str_arg = get_string_arg();
	size_t i = 0;
	while ((c = str_arg[i++]) != '\0') {
	  EnvInt w;
	  pr->set_ascii_char((unsigned char) c, current_env, &w);
	  current_env->hpos += w;
	}
	a_delete str_arg;
	break;
      }
    case 'u':			// u: print spaced word
      {
	char c;
	if (npages <= 0)
	  fatal_command(command);
	EnvInt kern = (EnvInt) get_integer_arg();
	char *str_arg = get_string_arg();
	size_t i = 0;
	while ((c = str_arg[i++]) != '\0') {
	  EnvInt w;
	  pr->set_ascii_char((unsigned char) c, current_env, &w);
	  current_env->hpos += w + kern;
	}
	a_delete str_arg;
	break;
      }
    case 'v':			// v: relative vertical move
      current_env->vpos += (EnvInt) get_integer_arg();
      break;
    case 'V':			// V: absolute vertical positioning
      current_env->vpos = (EnvInt) get_integer_arg();
      break;
    case 'w':			// w: inform about paddable space
      break;
    case 'x':			// device controlling commands
      stopped = parse_x_command();
      break;
    default:
      warning("unrecognized command `%1'", (unsigned char) command);
      skip_line();
      break;
    } // end of switch
  } // end of while

  // end of file reached
  if (npages > 0)
    pr->end_page(current_env->vpos);
  delete pr;
  pr = 0;
  fclose(current_file);
  // If `stopped' is not `true' here then there wasn't any `x stop'.
  if (!stopped)
    warning("no final `x stop' command");
  delete_current_env();
}