fixup for TITLE/SUBTITLE commit
[pspp] / src / output / driver.c
index 6dd25174fb5d7ec6569315b14605dc6345b8e473..f40876449b9ef473121a7075a656fca171a8188b 100644 (file)
@@ -1,5 +1,5 @@
 /* PSPP - a program for statistical analysis.
-   Copyright (C) 1997-9, 2000, 2007, 2009 Free Software Foundation, Inc.
+   Copyright (C) 1997-9, 2000, 2007, 2009, 2010, 2011, 2012, 2014, 2019 Free Software Foundation, Inc.
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
 
 #include <config.h>
 
-#include <output/driver.h>
-#include <output/driver-provider.h>
+#include "output/driver.h"
+#include "output/driver-provider.h"
 
 #include <ctype.h>
 #include <errno.h>
 #include <limits.h>
+#include <stdarg.h>
 #include <stdlib.h>
 #include <string.h>
-
-#include <data/file-name.h>
-#include <data/settings.h>
-#include <libpspp/array.h>
-#include <libpspp/assertion.h>
-#include <libpspp/string-map.h>
-#include <libpspp/string-set.h>
-#include <libpspp/str.h>
-#include <output/output-item.h>
-#include <output/text-item.h>
-
-#include "error.h"
-#include "xalloc.h"
-#include "xmemdup0.h"
+#include <time.h>
+
+#include "data/file-handle-def.h"
+#include "data/settings.h"
+#include "libpspp/array.h"
+#include "libpspp/assertion.h"
+#include "libpspp/i18n.h"
+#include "libpspp/message.h"
+#include "libpspp/llx.h"
+#include "libpspp/string-map.h"
+#include "libpspp/string-set.h"
+#include "libpspp/str.h"
+#include "output/output-item.h"
+
+#include "gl/error.h"
+#include "gl/xalloc.h"
+#include "gl/xmemdup0.h"
+#include "gl/xvasprintf.h"
 
 #include "gettext.h"
 #define _(msgid) gettext (msgid)
 
-static const struct output_driver_class *driver_classes[];
-
-static struct output_driver **drivers;
-static size_t n_drivers, allocated_drivers;
+struct output_engine
+  {
+    struct ll ll;                  /* Node for this engine. */
+    struct llx_list drivers;       /* Contains "struct output_driver"s. */
+    struct output_item *deferred_text; /* Output text being accumulated. */
+    char *command_name;            /* Name of command being processed. */
+    char *title, *subtitle;        /* Components of page title. */
+
+    /* Output grouping stack. */
+    struct output_item **groups;
+    size_t n_groups;
+    size_t allocated_groups;
+
+    struct string_map heading_vars;
+  };
 
-static unsigned int enabled_device_types = ((1u << OUTPUT_DEVICE_UNKNOWN)
-                                            | (1u << OUTPUT_DEVICE_LISTING)
-                                            | (1u << OUTPUT_DEVICE_SCREEN)
-                                            | (1u << OUTPUT_DEVICE_PRINTER));
+static struct ll_list engine_stack = LL_INITIALIZER (engine_stack);
 
-static struct output_item *deferred_syntax;
-static bool in_command;
+static const struct output_driver_factory *factories[];
 
-void
-output_close (void)
+static struct output_engine *
+engine_stack_top (void)
 {
-  while (n_drivers > 0)
-    {
-      struct output_driver *d = drivers[--n_drivers];
-      output_driver_destroy (d);
-    }
+  struct ll *head = ll_head (&engine_stack);
+  if (ll_is_empty (&engine_stack))
+    return NULL;
+  return ll_data (head, struct output_engine, ll);
 }
 
 static void
-expand_macro (const char *name, struct string *dst, void *macros_)
+put_strftime (const char *key, const char *format,
+              const struct tm *tm, struct string_map *vars)
 {
-  const struct string_map *macros = macros_;
-
-  if (!strcmp (name, "viewwidth"))
-    ds_put_format (dst, "%d", settings_get_viewwidth ());
-  else if (!strcmp (name, "viewlength"))
-    ds_put_format (dst, "%d", settings_get_viewlength ());
-  else
+  if (!string_map_find (vars, key))
     {
-      const char *value = string_map_find (macros, name);
-      if (value != NULL)
-        ds_put_cstr (dst, value);
+      char value[128];
+      strftime (value, sizeof value, format, tm);
+      string_map_insert (vars, key, value);
     }
 }
 
-/* Defines one configuration macro based on the text in BP, which
-   should be of the form `KEY=VALUE'.  Returns true if
-   successful, false if S is not in the proper form. */
-bool
-output_define_macro (const char *s, struct string_map *macros)
+void
+output_engine_push (void)
 {
-  const char *key_start, *value;
-  size_t key_len;
-  char *key;
-
-  s += strspn (s, CC_SPACES);
-
-  key_start = s;
-  key_len = strcspn (s, "=" CC_SPACES);
-  if (key_len == 0)
-    return false;
-  s += key_len;
-
-  s += strspn (s, CC_SPACES);
-  if (*s == '=')
-    s++;
+  struct output_engine *e = xzalloc (sizeof (*e));
 
-  s += strspn (s, CC_SPACES);
-  value = s;
-
-  key = xmemdup0 (key_start, key_len);
-  if (!string_map_contains (macros, key))
-    {
-      struct string expanded_value = DS_EMPTY_INITIALIZER;
-
-      fn_interp_vars (ss_cstr (value), expand_macro, &macros, &expanded_value);
-      string_map_insert_nocopy (macros, key, ds_steal_cstr (&expanded_value));
-    }
-  else
-    free (key);
+  llx_init (&e->drivers);
 
-  return true;
-}
+  string_map_init (&e->heading_vars);
 
-static void
-add_driver_names (char *to, struct string_set *names)
-{
-  char *save_ptr = NULL;
-  char *name;
+  time_t t = time (NULL);
+  const struct tm *tm = localtime (&t);
+  put_strftime ("Date", "%x", tm, &e->heading_vars);
+  put_strftime ("Time", "%X", tm, &e->heading_vars);
 
-  for (name = strtok_r (to, CC_SPACES, &save_ptr); name != NULL;
-       name = strtok_r (NULL, CC_SPACES, &save_ptr))
-    string_set_insert (names, name);
+  ll_push_head (&engine_stack, &e->ll);
 }
 
-static void
-init_default_drivers (void)
+void
+output_engine_pop (void)
 {
-  error (0, 0, _("using default output driver configuration"));
-  output_configure_driver ("list:ascii:listing:"
-                           "length=66 width=79 output-file=\"pspp.list\"");
+  struct ll *head = ll_pop_head (&engine_stack);
+  struct output_engine *e = ll_data (head, struct output_engine, ll);
+
+  struct output_driver *d;
+  llx_for_each_preremove (d, &e->drivers, &llx_malloc_mgr)
+    output_driver_destroy (d);
+  output_item_unref (e->deferred_text);
+  free (e->command_name);
+  free (e->title);
+  free (e->subtitle);
+  if (e->n_groups)
+    output_item_unref (e->groups[0]);
+  free (e->groups);
+  string_map_destroy (&e->heading_vars);
+  free (e);
 }
 
-static void
-warn_unused_drivers (const struct string_set *unused_drivers,
-                     const struct string_set *requested_drivers)
+void
+output_get_supported_formats (struct string_set *formats)
 {
-  const struct string_set_node *node;
-  const char *name;
+  const struct output_driver_factory **fp;
 
-  STRING_SET_FOR_EACH (name, node, unused_drivers)
-    if (string_set_contains (requested_drivers, name))
-      error (0, 0, _("unknown output driver `%s'"), name);
-    else
-      error (0, 0, _("output driver `%s' referenced but never defined"), name);
+  for (fp = factories; *fp != NULL; fp++)
+    string_set_insert (formats, (*fp)->extension);
 }
 
-void
-output_read_configuration (const struct string_map *macros_,
-                           const struct string_set *driver_names_)
+static bool
+output_driver_should_show (const struct output_driver *d,
+                           const struct output_item *item)
 {
-  struct string_map macros = STRING_MAP_INITIALIZER (macros);
-  struct string_set driver_names = STRING_SET_INITIALIZER (driver_names);
-  char *devices_file_name = NULL;
-  FILE *devices_file = NULL;
-  struct string line = DS_EMPTY_INITIALIZER;
-  int line_number;
+  enum settings_output_type type = SETTINGS_OUTPUT_RESULT;
+  switch (item->type)
+    {
+    case OUTPUT_ITEM_MESSAGE:
+      type = (item->message->severity == MSG_S_NOTE
+              ? SETTINGS_OUTPUT_NOTE
+              : SETTINGS_OUTPUT_ERROR);
+      break;
 
-  ds_init_empty (&line);
+    case OUTPUT_ITEM_TEXT:
+      if (item->text.subtype == TEXT_ITEM_SYNTAX)
+        type = SETTINGS_OUTPUT_SYNTAX;
+      break;
 
-  devices_file_name = fn_search_path ("devices", config_path);
-  if (devices_file_name == NULL)
-    {
-      error (0, 0, _("cannot find output initialization file "
-                     "(use `-vv' to view search path)"));
-      goto exit;
-    }
-  devices_file = fopen (devices_file_name, "r");
-  if (devices_file == NULL)
-    {
-      error (0, errno, _("cannot open \"%s\""), devices_file_name);
-      goto exit;
+    case OUTPUT_ITEM_CHART:
+    case OUTPUT_ITEM_GROUP:
+    case OUTPUT_ITEM_IMAGE:
+    case OUTPUT_ITEM_PAGE_BREAK:
+    case OUTPUT_ITEM_TABLE:
+      break;
     }
 
-  string_map_replace_map (&macros, macros_);
-  string_set_union (&driver_names, driver_names_);
-  if (string_set_is_empty (&driver_names))
-    string_set_insert (&driver_names, "default");
+  return (settings_get_output_routing (type) & d->device_type) != 0;
+}
 
-  line_number = 0;
-  for (;;)
+/* Adds to OUT the subset of IN that driver D should show, considering routing
+   and visibility of each item, and flattening groups for drivers that don't
+   handle them internally. */
+static void
+make_driver_output_subset (const struct output_item *in,
+                           const struct output_driver *d,
+                           struct output_item *out)
+{
+  if (in->type == OUTPUT_ITEM_GROUP)
     {
-      char *cp, *delimiter, *name;
-
-      if (!ds_read_config_line (&line, &line_number, devices_file))
-       {
-         if (ferror (devices_file))
-           error (0, errno, _("reading \"%s\""), devices_file_name);
-         break;
-       }
-
-      cp = ds_cstr (&line);
-      cp += strspn (cp, CC_SPACES);
-
-      if (*cp == '\0')
-        continue;
-      else if (!strncmp ("define", cp, 6) && isspace ((unsigned char) cp[6]))
+      /* If we should include the group itself, then clone IN inside OUT, and
+         add any children to the clone instead to OUT directly. */
+      if (output_driver_should_show (d, in) && d->class->handles_groups)
         {
-          if (!output_define_macro (&cp[7], &macros))
-            error_at_line (0, 0, devices_file_name, line_number,
-                           _("\"%s\" is not a valid macro definition"),
-                           &cp[7]);
-          continue;
+          struct output_item *group = group_item_clone_empty (in);
+          group_item_add_child (out, group);
+          out = group;
         }
 
-      delimiter = cp + strcspn (cp, ":=");
-      name = xmemdup0 (cp, delimiter - cp);
-      if (*delimiter == '=')
-        {
-          if (string_set_delete (&driver_names, name))
-            add_driver_names (delimiter + 1, &driver_names);
-        }
-      else if (*delimiter == ':')
-        {
-          if (string_set_delete (&driver_names, name))
-            {
-              fn_interp_vars (ds_ss (&line), expand_macro, &macros, &line);
-              output_configure_driver (ds_cstr (&line));
-            }
-        }
-      else
-        error_at_line (0, 0, devices_file_name, line_number,
-                       _("syntax error"));
-      free (name);
+      for (size_t i = 0; i < in->group.n_children; i++)
+        make_driver_output_subset (in->group.children[i], d, out);
     }
-
-  warn_unused_drivers (&driver_names, driver_names_);
-
-exit:
-  if (devices_file != NULL)
-    fclose (devices_file);
-  free (devices_file_name);
-  ds_destroy (&line);
-  string_set_destroy (&driver_names);
-  string_map_destroy (&macros);
-
-  if (n_drivers == 0)
+  else
     {
-      error (0, 0, _("no active output drivers"));
-      init_default_drivers ();
+      if (output_driver_should_show (d, in)
+          && (in->show || d->class->handles_show))
+        group_item_add_child (out, output_item_ref (in));
     }
 }
 
-/* Obtains a token from S and advances its position.  Errors are
-   reported against the given DRIVER_NAME.
-   The token is stored in TOKEN.  Returns true if successful,
-   false on syntax error.
-
-   Caller is responsible for skipping leading spaces. */
-static bool
-get_option_token (char **s_, const char *driver_name,
-                  struct string *token)
+static void
+output_submit__ (struct output_engine *e, struct output_item *item)
 {
-  struct substring s = ss_cstr (*s_);
-  int c;
-
-  ds_clear (token);
-  c = ss_get_char (&s);
-  if (c == EOF)
+  if (e->n_groups > 0)
     {
-      error (0, 0, _("syntax error parsing options for \"%s\" driver"),
-             driver_name);
-      return false;
+      group_item_add_child (e->groups[e->n_groups - 1], item);
+      return;
     }
-  else if (c == '\'' || c == '"')
-    {
-      int quote = c;
-
-      for (;;)
-        {
-          c = ss_get_char (&s);
-          if (c == quote)
-            break;
-          else if (c == EOF)
-            {
-              error (0, 0,
-                     _("reached end of options inside quoted string "
-                       "parsing options for \"%s\" driver"),
-                     driver_name);
-              return false;
-            }
-          else if (c != '\\')
-            ds_put_char (token, c);
-          else
-            {
-              int out;
 
-              c = ss_get_char (&s);
-              switch (c)
-                {
-                case '\'':
-                  out = '\'';
-                  break;
-                case '"':
-                  out = '"';
-                  break;
-                case '\\':
-                  out = '\\';
-                  break;
-                case 'a':
-                  out = '\a';
-                  break;
-                case 'b':
-                  out = '\b';
-                  break;
-                case 'f':
-                  out = '\f';
-                  break;
-                case 'n':
-                  out = '\n';
-                  break;
-                case 'r':
-                  out = '\r';
-                  break;
-                case 't':
-                  out = '\t';
-                  break;
-                case 'v':
-                  out = '\v';
-                  break;
-                case '0':
-                case '1':
-                case '2':
-                case '3':
-                case '4':
-                case '5':
-                case '6':
-                case '7':
-                  out = c - '0';
-                  while (ss_first (s) >= '0' && ss_first (s) <= '7')
-                    out = out * 8 + (ss_get_char (&s) - '0');
-                  break;
-                case 'x':
-                case 'X':
-                  out = 0;
-                  while (isxdigit (ss_first (s)))
-                    {
-                      c = ss_get_char (&s);
-                      out *= 16;
-                      if (isdigit (c))
-                        out += c - '0';
-                      else
-                        out += tolower (c) - 'a' + 10;
-                    }
-                  break;
-                default:
-                  error (0, 0, _("syntax error in string constant "
-                                 "parsing options for \"%s\" driver"),
-                         driver_name);
-                  return false;
-                }
-              ds_put_char (token, out);
-            }
-        }
-    }
-  else
+  struct llx *llx, *next;
+  llx_for_each_safe (llx, next, &e->drivers)
     {
-      for (;;)
-        {
-          ds_put_char (token, c);
+      struct output_driver *d = llx_data (llx);
 
-          c = ss_first (s);
-          if (c == EOF || c == '=' || isspace (c))
-            break;
-          ss_advance (&s, 1);
-        }
+      struct output_item *root = root_item_create ();
+      make_driver_output_subset (item, d, root);
+      for (size_t i = 0; i < root->group.n_children; i++)
+        d->class->submit (d, root->group.children[i]);
+      output_item_unref (root);
     }
 
-  *s_ = s.string;
-  return 1;
+  output_item_unref (item);
 }
 
 static void
-parse_options (const char *driver_name, char *options,
-               struct string_map *option_map)
+flush_deferred_text (struct output_engine *e)
 {
-  struct string key = DS_EMPTY_INITIALIZER;
-  struct string value = DS_EMPTY_INITIALIZER;
-
-  for (;;)
+  struct output_item *deferred_text = e->deferred_text;
+  if (deferred_text)
     {
-      options += strspn (options, CC_SPACES);
-      if (*options == '\0')
-        break;
-
-      if (!get_option_token (&options, driver_name, &key))
-        break;
-
-      options += strspn (options, CC_SPACES);
-      if (*options != '=')
-       {
-         error (0, 0, _("syntax error expecting `=' "
-                         "parsing options for driver \"%s\""),
-                 driver_name);
-         break;
-       }
-      options++;
-
-      options += strspn (options, CC_SPACES);
-      if (!get_option_token (&options, driver_name, &value))
-        break;
-
-      if (string_map_contains (option_map, ds_cstr (&key)))
-        error (0, 0, _("driver \"%s\" defines option \"%s\" multiple times"),
-               driver_name, ds_cstr (&key));
-      else
-        string_map_insert (option_map, ds_cstr (&key), ds_cstr (&value));
+      e->deferred_text = NULL;
+      output_submit__ (e, deferred_text);
     }
-
-  ds_destroy (&key);
-  ds_destroy (&value);
 }
 
-static char *
-trim_token (char *token)
+static bool
+defer_text (struct output_engine *e, struct output_item *item)
 {
-  if (token != NULL)
-    {
-      char *end;
-
-      /* Trim leading spaces. */
-      while (isspace ((unsigned char) *token))
-        token++;
-
-      /* Trim trailing spaces. */
-      end = strchr (token, '\0');
-      while (end > token && isspace ((unsigned char) end[-1]))
-        *--end = '\0';
+  if (item->type != OUTPUT_ITEM_TEXT)
+    return false;
 
-      /* An empty token is a null token. */
-      if (*token == '\0')
-        return NULL;
+  if (!e->deferred_text)
+    e->deferred_text = output_item_unshare (item);
+  else if (text_item_append (e->deferred_text, item))
+    output_item_unref (item);
+  else
+    {
+      flush_deferred_text (e);
+      e->deferred_text = output_item_unshare (item);
     }
-  return token;
+  return true;
 }
 
-static const struct output_driver_class *
-find_output_class (const char *name)
+/* Submits ITEM to the configured output drivers, and transfers ownership to
+   the output subsystem. */
+void
+output_submit (struct output_item *item)
 {
-  const struct output_driver_class **classp;
+  struct output_engine *e = engine_stack_top ();
 
-  for (classp = driver_classes; *classp != NULL; classp++)
-    if (!strcmp ((*classp)->name, name))
-      break;
+  if (e == NULL)
+    return;
 
-  return *classp;
+  if (item == NULL)
+    return;
+
+  if (defer_text (e, item))
+    return;
+  flush_deferred_text (e);
+
+  /* XXX heading_vars */
+
+  output_submit__ (e, item);
 }
 
-static struct output_driver *
-create_driver (const char *driver_name, const char *class_name,
-               const char *device_type, struct string_map *options)
+/* Returns the name of the command currently being parsed, or NULL if no
+   command is being parsed. */
+const char *
+output_get_command_name (void)
 {
-  const struct output_driver_class *class;
-  enum output_device_type type;
-  struct output_driver *driver;
+  struct output_engine *e = engine_stack_top ();
+  if (e == NULL)
+    return NULL;
 
-  type = OUTPUT_DEVICE_UNKNOWN;
-  if (device_type != NULL && device_type[0] != '\0')
-    {
-      if (!strcmp (device_type, "listing"))
-        type = OUTPUT_DEVICE_LISTING;
-      else if (!strcmp (device_type, "screen"))
-        type = OUTPUT_DEVICE_SCREEN;
-      else if (!strcmp (device_type, "printer"))
-        type = OUTPUT_DEVICE_PRINTER;
-      else
-        error (0, 0, _("unknown device type `%s'"), device_type);
-    }
+  for (size_t i = e->n_groups; i-- > 0;)
+    if (e->groups[i]->command_name)
+      return e->groups[i]->command_name;
 
-  class = find_output_class (class_name);
-  if (class != NULL)
-    driver = class->create (driver_name, type, options);
-  else
-    {
-      error (0, 0, _("unknown output driver class `%s'"), class_name);
-      driver = NULL;
-    }
-
-  string_map_destroy (options);
+  return NULL;
+}
 
-  return driver;
+char *
+output_get_uppercase_command_name (void)
+{
+  const char *command_name = output_get_command_name ();
+  return command_name ? utf8_to_upper (command_name) : NULL;
 }
 
-struct output_driver *
-output_driver_create (const char *class_name, struct string_map *options)
+size_t
+output_open_group (struct output_item *item)
 {
-  return create_driver (class_name, class_name, NULL, options);
+  struct output_engine *e = engine_stack_top ();
+  if (e == NULL)
+    return 0;
+
+  if (e->n_groups >= e->allocated_groups)
+    e->groups = x2nrealloc (e->groups, &e->allocated_groups,
+                            sizeof *e->groups);
+  e->groups[e->n_groups++] = item;
+  if (e->n_groups > 1)
+    group_item_add_child (e->groups[e->n_groups - 2], item);
+
+  return e->n_groups - 1;
 }
 
-/* String LINE is in format:
-   DRIVERNAME:CLASSNAME:DEVICETYPE:OPTIONS
-*/
 void
-output_configure_driver (const char *line_)
+output_close_groups (size_t nesting_level)
 {
-  char *save_ptr = NULL;
-  char *line = xstrdup (line_);
-  char *driver_name = trim_token (strtok_r (line, ":", &save_ptr));
-  char *class_name = trim_token (strtok_r (NULL, ":", &save_ptr));
-  char *device_type = trim_token (strtok_r (NULL, ":", &save_ptr));
-  char *options = trim_token (strtok_r (NULL, "", &save_ptr));
+  struct output_engine *e = engine_stack_top ();
+  if (e == NULL)
+    return;
 
-  if (driver_name && class_name)
+  while (e->n_groups > nesting_level)
     {
-      struct string_map option_map;
-      struct output_driver *driver;
-
-      string_map_init (&option_map);
-      if (options != NULL)
-        parse_options (driver_name, options, &option_map);
+      flush_deferred_text (e);
 
-      driver = create_driver (driver_name, class_name,
-                              device_type, &option_map);
-      if (driver != NULL)
-        output_driver_register (driver);
+      struct output_item *group = e->groups[--e->n_groups];
+      if (e->n_groups == 0)
+        output_submit__ (e, group);
     }
-  else
-    error (0, 0,
-           _("driver definition line missing driver name or class name"));
-
-  free (line);
 }
 
-/* Display on stdout a list of all registered driver classes. */
+/* Flushes output to screen devices, so that the user can see
+   output that doesn't fill up an entire page. */
 void
-output_list_classes (void)
+output_flush (void)
 {
-  const struct output_driver_class **classp;
+  struct output_engine *e = engine_stack_top ();
 
-  printf (_("Driver classes:"));
-  for (classp = driver_classes; *classp != NULL; classp++)
-    printf (" %s", (*classp)->name);
-  putc ('\n', stdout);
-}
+  flush_deferred_text (e);
 
-static bool
-driver_is_enabled (const struct output_driver *d)
-{
-  return (1u << d->device_type) & enabled_device_types;
+  struct llx *llx;
+  llx_for_each (llx, &e->drivers)
+    {
+      struct output_driver *d = llx_data (llx);
+      if (d->device_type & SETTINGS_DEVICE_TERMINAL && d->class->flush != NULL)
+        d->class->flush (d);
+    }
 }
 
 static void
-output_submit__ (struct output_item *item)
+output_set_title__ (struct output_engine *e, char **dst, const char *src)
 {
-  size_t i;
-
-  for (i = 0; i < n_drivers; i++)
-    {
-      struct output_driver *d = drivers[i];
-      if (driver_is_enabled (d))
-        d->class->submit (d, item);
-    }
-
-  output_item_unref (item);
+  free (*dst);
+  *dst = xstrdup_if_nonnull (src);
+
+  char *page_title
+    = (e->title && e->subtitle ? xasprintf ("%s\n%s", e->title, e->subtitle)
+       : e->title ? xstrdup (e->title)
+       : e->subtitle ? xstrdup (e->subtitle)
+       : xzalloc (1));
+  output_item_submit (text_item_create_nocopy (TEXT_ITEM_PAGE_TITLE,
+                                               page_title, NULL));
 }
 
-/* Submits ITEM to the configured output drivers, and transfers ownership to
-   the output subsystem. */
-void
-output_submit (struct output_item *item)
+void PRINTF_FORMAT (1, 2)
+output_log (const char *format, ...)
 {
-  if (is_text_item (item))
-    {
-      struct text_item *text = to_text_item (item);
-      switch (text_item_get_type (text))
-        {
-        case TEXT_ITEM_SYNTAX:
-          if (!in_command)
-            {
-              if (deferred_syntax != NULL)
-                output_submit__ (deferred_syntax);
-              deferred_syntax = item;
-              return;
-            }
-          break;
+  va_list args;
+  va_start (args, format);
+  char *s = xvasprintf (format, args);
+  va_end (args);
 
-        case TEXT_ITEM_COMMAND_OPEN:
-          output_submit__ (item);
-          if (deferred_syntax != NULL)
-            {
-              output_submit__ (deferred_syntax);
-              deferred_syntax = NULL;
-            }
-          in_command = true;
-          return;
-
-        case TEXT_ITEM_COMMAND_CLOSE:
-          in_command = false;
-          break;
-
-        default:
-          break;
-        }
-    }
+  output_submit (text_item_create_nocopy (TEXT_ITEM_LOG, s, NULL));
+}
 
-  output_submit__ (item);
+const char *
+output_get_title (void)
+{
+  return engine_stack_top ()->title;
 }
 
-/* Flushes output to screen devices, so that the user can see
-   output that doesn't fill up an entire page. */
 void
-output_flush (void)
+output_set_title (const char *title)
 {
-  size_t i;
+  struct output_engine *e = engine_stack_top ();
 
-  for (i = 0; i < n_drivers; i++)
-    {
-      struct output_driver *d = drivers[i];
-      if (driver_is_enabled (d) && d->class->flush != NULL)
-        d->class->flush (d);
-    }
+  output_set_title__ (e, &e->title, title);
 }
 
-unsigned int
-output_get_enabled_types (void)
+const char *
+output_get_subtitle (void)
 {
-  return enabled_device_types;
+  return engine_stack_top ()->subtitle;
 }
 
 void
-output_set_enabled_types (unsigned int types)
+output_set_subtitle (const char *subtitle)
 {
-  enabled_device_types = types;
+  struct output_engine *e = engine_stack_top ();
+
+  output_set_title__ (e, &e->subtitle, subtitle);
 }
 
 void
-output_set_type_enabled (bool enable, enum output_device_type type)
+output_set_filename (const char *filename)
 {
-  unsigned int bit = 1u << type;
-  if (enable)
-    enabled_device_types |= bit;
-  else
-    enabled_device_types |= ~bit;
+  struct output_engine *e = engine_stack_top ();
+
+  string_map_replace (&e->heading_vars, "Filename", filename);
 }
 \f
 void
 output_driver_init (struct output_driver *driver,
                     const struct output_driver_class *class,
-                    const char *name, enum output_device_type type)
+                    const char *name, enum settings_output_devices type)
 {
   driver->class = class;
   driver->name = xstrdup (name);
@@ -672,51 +435,227 @@ output_driver_get_name (const struct output_driver *driver)
   return driver->name;
 }
 \f
+static struct output_engine *
+output_driver_get_engine (const struct output_driver *driver)
+{
+  struct output_engine *e;
+
+  ll_for_each (e, struct output_engine, ll, &engine_stack)
+    {
+      if (llx_find (llx_head (&e->drivers), llx_null (&e->drivers), driver))
+       return e;
+    }
+
+  return NULL;
+}
+
 void
 output_driver_register (struct output_driver *driver)
 {
+  struct output_engine *e = engine_stack_top ();
+
   assert (!output_driver_is_registered (driver));
-  if (n_drivers >= allocated_drivers)
-    drivers = x2nrealloc (drivers, &allocated_drivers, sizeof *drivers);
-  drivers[n_drivers++] = driver;
+  llx_push_tail (&e->drivers, driver, &llx_malloc_mgr);
 }
 
 void
 output_driver_unregister (struct output_driver *driver)
 {
-  size_t i;
+  struct output_engine *e = output_driver_get_engine (driver);
 
-  for (i = 0; i < n_drivers; i++)
-    if (drivers[i] == driver)
-      {
-        remove_element (drivers, n_drivers, sizeof *drivers, i);
-        return;
-      }
-  NOT_REACHED ();
+  assert (e != NULL);
+  llx_remove (llx_find (llx_head (&e->drivers), llx_null (&e->drivers), driver),
+              &llx_malloc_mgr);
 }
 
 bool
 output_driver_is_registered (const struct output_driver *driver)
 {
-  size_t i;
+  return output_driver_get_engine (driver) != NULL;
+}
 
-  for (i = 0; i < n_drivers; i++)
-    if (drivers[i] == driver)
-      return true;
-  return false;
+void
+output_set_page_setup (const struct page_setup *ps)
+{
+  struct output_engine *e = engine_stack_top ();
+
+  struct llx *llx;
+  llx_for_each (llx, &e->drivers)
+    {
+      struct output_driver *d = llx_data (llx);
+      if (d->class->setup)
+        d->class->setup (d, ps);
+    }
 }
 \f
-/* Known driver classes. */
-
-static const struct output_driver_class *driver_classes[] =
+extern const struct output_driver_factory csv_driver_factory;
+extern const struct output_driver_factory html_driver_factory;
+extern const struct output_driver_factory list_driver_factory;
+extern const struct output_driver_factory odt_driver_factory;
+extern const struct output_driver_factory pdf_driver_factory;
+extern const struct output_driver_factory png_driver_factory;
+extern const struct output_driver_factory ps_driver_factory;
+extern const struct output_driver_factory spv_driver_factory;
+extern const struct output_driver_factory svg_driver_factory;
+extern const struct output_driver_factory tex_driver_factory;
+extern const struct output_driver_factory txt_driver_factory;
+
+static const struct output_driver_factory *factories[] =
   {
-    &ascii_class,
-#ifdef HAVE_CAIRO
-    &cairo_class,
-#endif
-    &html_class,
-    &odt_class,
-    &csv_class,
-    NULL,
+    &txt_driver_factory,
+    &list_driver_factory,
+    &html_driver_factory,
+    &csv_driver_factory,
+    &odt_driver_factory,
+    &spv_driver_factory,
+    &pdf_driver_factory,
+    &ps_driver_factory,
+    &svg_driver_factory,
+    &png_driver_factory,
+    &tex_driver_factory,
+    NULL
   };
 
+static const struct output_driver_factory *
+find_factory (const char *format)
+{
+  const struct output_driver_factory **fp;
+
+  for (fp = factories; *fp != NULL; fp++)
+    {
+      const struct output_driver_factory *f = *fp;
+
+      if (!strcmp (f->extension, format))
+        return f;
+    }
+  return &txt_driver_factory;
+}
+
+static enum settings_output_devices
+default_device_type (const char *file_name)
+{
+  return (!strcmp (file_name, "-")
+          ? SETTINGS_DEVICE_TERMINAL
+          : SETTINGS_DEVICE_LISTING);
+}
+
+struct output_driver *
+output_driver_create (struct string_map *options)
+{
+  enum settings_output_devices device_type;
+  const struct output_driver_factory *f;
+  struct output_driver *driver;
+  char *device_string;
+  char *file_name;
+  char *format;
+
+  format = string_map_find_and_delete (options, "format");
+  file_name = string_map_find_and_delete (options, "output-file");
+
+  if (format == NULL)
+    {
+      if (file_name != NULL)
+        {
+          const char *extension = strrchr (file_name, '.');
+          format = xstrdup (extension != NULL ? extension + 1 : "");
+        }
+      else
+        format = xstrdup ("txt");
+    }
+  f = find_factory (format);
+
+  if (file_name == NULL)
+    file_name = xstrdup (f->default_file_name);
+
+  /* XXX should use parse_enum(). */
+  device_string = string_map_find_and_delete (options, "device");
+  if (device_string == NULL || device_string[0] == '\0')
+    device_type = default_device_type (file_name);
+  else if (!strcmp (device_string, "terminal"))
+    device_type = SETTINGS_DEVICE_TERMINAL;
+  else if (!strcmp (device_string, "listing"))
+    device_type = SETTINGS_DEVICE_LISTING;
+  else
+    {
+      msg (MW, _("%s is not a valid device type (the choices are `%s' and `%s')"),
+                     device_string, "terminal", "listing");
+      device_type = default_device_type (file_name);
+    }
+
+  struct file_handle *fh = fh_create_file (NULL, file_name, NULL, fh_default_properties ());
+
+  driver = f->create (fh, device_type, options);
+  if (driver != NULL)
+    {
+      const struct string_map_node *node;
+      const char *key;
+
+      STRING_MAP_FOR_EACH_KEY (key, node, options)
+        msg (MW, _("%s: unknown option `%s'"), file_name, key);
+    }
+  string_map_clear (options);
+
+  free (file_name);
+  free (format);
+  free (device_string);
+
+  return driver;
+}
+
+void
+output_driver_parse_option (const char *option, struct string_map *options)
+{
+  const char *equals = strchr (option, '=');
+  if (equals == NULL)
+    {
+      error (0, 0, _("%s: output option missing `='"), option);
+      return;
+    }
+
+  char *key = xmemdup0 (option, equals - option);
+  if (string_map_contains (options, key))
+    {
+      error (0, 0, _("%s: output option specified more than once"), key);
+      free (key);
+      return;
+    }
+
+  char *value = xmemdup0 (equals + 1, strlen (equals + 1));
+  string_map_insert_nocopy (options, key, value);
+}
+\f
+char *
+output_driver_substitute_heading_vars (const char *src, int page_number)
+{
+  struct output_engine *e = engine_stack_top ();
+  struct string dst = DS_EMPTY_INITIALIZER;
+  ds_extend (&dst, strlen (src));
+  for (const char *p = src; *p;)
+    {
+      if (!strncmp (p, "&amp;[", 6))
+        {
+          if (page_number != INT_MIN)
+            {
+              const char *start = p + 6;
+              const char *end = strchr (start, ']');
+              if (end)
+                {
+                  const char *value = string_map_find__ (&e->heading_vars,
+                                                         start, end - start);
+                  if (value)
+                    ds_put_cstr (&dst, value);
+                  else if (ss_equals (ss_buffer (start, end - start),
+                                      ss_cstr ("Page")))
+                    ds_put_format (&dst, "%d", page_number);
+                  p = end + 1;
+                  continue;
+                }
+            }
+          ds_put_cstr (&dst, "&amp;");
+          p += 5;
+        }
+      else
+        ds_put_byte (&dst, *p++);
+    }
+  return ds_steal_cstr (&dst);
+}