Add support for reading and writing SPV files.
[pspp] / src / output / driver.c
index 6dd25174fb5d7ec6569315b14605dc6345b8e473..b18b822f22ca4b1f99d0e66736b8f345e5185d2f 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 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 <libxml/parser.h>
+#include <libxml/tree.h>
 #include <limits.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/message.h"
+#include "libpspp/llx.h"
+#include "libpspp/string-map.h"
+#include "libpspp/string-set.h"
+#include "libpspp/str.h"
+#include "output/group-item.h"
+#include "output/message-item.h"
+#include "output/output-item.h"
+#include "output/text-item.h"
+
+#include "gl/error.h"
+#include "gl/xalloc.h"
+#include "gl/xmemdup0.h"
 
 #include "gettext.h"
 #define _(msgid) gettext (msgid)
 
-static const struct output_driver_class *driver_classes[];
+struct output_engine
+  {
+    struct llx_list drivers;       /* Contains "struct output_driver"s. */
+    struct string deferred_text;   /* Output text being accumulated. */
+    enum text_item_type deferred_type; /* Type of text being accumulated. */
+    char *command_name;            /* Name of command being processed. */
+    char *title, *subtitle;        /* Components of page title. */
+
+    /* Output grouping stack.
 
-static struct output_driver **drivers;
-static size_t n_drivers, allocated_drivers;
+       TEXT_ITEM_GROUP_OPEN pushes a group on the stack and
+       TEXT_ITEM_GROUP_CLOSE pops one off. */
+    char **groups;               /* Command names of nested sections. */
+    size_t n_groups;
+    size_t allocated_groups;
 
-static unsigned int enabled_device_types = ((1u << OUTPUT_DEVICE_UNKNOWN)
-                                            | (1u << OUTPUT_DEVICE_LISTING)
-                                            | (1u << OUTPUT_DEVICE_SCREEN)
-                                            | (1u << OUTPUT_DEVICE_PRINTER));
+    struct string_map heading_vars;
+  };
 
-static struct output_item *deferred_syntax;
-static bool in_command;
+static const struct output_driver_factory *factories[];
 
-void
-output_close (void)
+/* A stack of output engines.. */
+static struct output_engine *engine_stack;
+static size_t n_stack, allocated_stack;
+
+static struct output_engine *
+engine_stack_top (void)
 {
-  while (n_drivers > 0)
-    {
-      struct output_driver *d = drivers[--n_drivers];
-      output_driver_destroy (d);
-    }
+  assert (n_stack > 0);
+  return &engine_stack[n_stack - 1];
 }
 
 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);
+  struct output_engine *e;
 
-  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++;
+  if (n_stack >= allocated_stack)
+    engine_stack = x2nrealloc (engine_stack, &allocated_stack,
+                               sizeof *engine_stack);
 
-  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;
+  e = &engine_stack[n_stack++];
+  memset (e, 0, sizeof *e);
+  llx_init (&e->drivers);
+  ds_init_empty (&e->deferred_text);
 
-      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);
+  string_map_init (&e->heading_vars);
 
-  return true;
-}
-
-static void
-add_driver_names (char *to, struct string_set *names)
-{
-  char *save_ptr = NULL;
-  char *name;
-
-  for (name = strtok_r (to, CC_SPACES, &save_ptr); name != NULL;
-       name = strtok_r (NULL, CC_SPACES, &save_ptr))
-    string_set_insert (names, name);
-}
-
-static void
-init_default_drivers (void)
-{
-  error (0, 0, _("using default output driver configuration"));
-  output_configure_driver ("list:ascii:listing:"
-                           "length=66 width=79 output-file=\"pspp.list\"");
-}
-
-static void
-warn_unused_drivers (const struct string_set *unused_drivers,
-                     const struct string_set *requested_drivers)
-{
-  const struct string_set_node *node;
-  const char *name;
-
-  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);
+  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);
 }
 
 void
-output_read_configuration (const struct string_map *macros_,
-                           const struct string_set *driver_names_)
+output_engine_pop (void)
 {
-  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;
-
-  ds_init_empty (&line);
+  struct output_engine *e;
 
-  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)
+  assert (n_stack > 0);
+  e = &engine_stack[--n_stack];
+  while (!llx_is_empty (&e->drivers))
     {
-      error (0, errno, _("cannot open \"%s\""), devices_file_name);
-      goto exit;
-    }
-
-  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");
-
-  line_number = 0;
-  for (;;)
-    {
-      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 (!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;
-        }
-
-      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);
+      struct output_driver *d = llx_pop_head (&e->drivers, &llx_malloc_mgr);
+      output_driver_destroy (d);
     }
+  ds_destroy (&e->deferred_text);
+  free (e->command_name);
+  free (e->title);
+  free (e->subtitle);
+  for (size_t i = 0; i < e->n_groups; i++)
+    free (e->groups[i]);
+  free (e->groups);
+  string_map_destroy (&e->heading_vars);
+}
 
-  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);
+void
+output_get_supported_formats (struct string_set *formats)
+{
+  const struct output_driver_factory **fp;
 
-  if (n_drivers == 0)
-    {
-      error (0, 0, _("no active output drivers"));
-      init_default_drivers ();
-    }
+  for (fp = factories; *fp != NULL; fp++)
+    string_set_insert (formats, (*fp)->extension);
 }
 
-/* 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;
+  struct llx *llx, *next;
 
-  ds_clear (token);
-  c = ss_get_char (&s);
-  if (c == EOF)
+  for (llx = llx_head (&e->drivers); llx != llx_null (&e->drivers); llx = next)
     {
-      error (0, 0, _("syntax error parsing options for \"%s\" driver"),
-             driver_name);
-      return false;
-    }
-  else if (c == '\'' || c == '"')
-    {
-      int quote = c;
+      struct output_driver *d = llx_data (llx);
+      enum settings_output_type type;
+
+      next = llx_next (llx);
 
-      for (;;)
+      if (is_message_item (item))
         {
-          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);
+          const struct msg *m = message_item_get_msg (to_message_item (item));
+          if (m->severity == MSG_S_NOTE)
+            type = SETTINGS_OUTPUT_NOTE;
           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);
-            }
+            type = SETTINGS_OUTPUT_ERROR;
         }
-    }
-  else
-    {
-      for (;;)
-        {
-          ds_put_char (token, c);
+      else if (is_text_item (item)
+               && text_item_get_type (to_text_item (item)) == TEXT_ITEM_SYNTAX)
+        type = SETTINGS_OUTPUT_SYNTAX;
+      else
+        type = SETTINGS_OUTPUT_RESULT;
 
-          c = ss_first (s);
-          if (c == EOF || c == '=' || isspace (c))
-            break;
-          ss_advance (&s, 1);
-        }
+      if (settings_get_output_routing (type) & d->device_type)
+        d->class->submit (d, item);
     }
 
-  *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 (;;)
+  if (!ds_is_empty (&e->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));
+      char *text = ds_steal_cstr (&e->deferred_text);
+      output_submit__ (e, text_item_super (text_item_create_nocopy (
+                                             e->deferred_type, 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;
+  if (!is_text_item (item))
+    return false;
 
-      /* Trim leading spaces. */
-      while (isspace ((unsigned char) *token))
-        token++;
+  struct text_item *text_item = to_text_item (item);
+  if (text_item->markup)        /* XXX */
+    return false;
 
-      /* Trim trailing spaces. */
-      end = strchr (token, '\0');
-      while (end > token && isspace ((unsigned char) end[-1]))
-        *--end = '\0';
+  enum text_item_type type = text_item_get_type (text_item);
+  if (type != TEXT_ITEM_SYNTAX && type != TEXT_ITEM_LOG)
+    return false;
 
-      /* An empty token is a null token. */
-      if (*token == '\0')
-        return NULL;
-    }
-  return token;
-}
+  if (!ds_is_empty (&e->deferred_text) && e->deferred_type != type)
+    flush_deferred_text (e);
 
-static const struct output_driver_class *
-find_output_class (const char *name)
-{
-  const struct output_driver_class **classp;
+  e->deferred_type = type;
 
-  for (classp = driver_classes; *classp != NULL; classp++)
-    if (!strcmp ((*classp)->name, name))
-      break;
+  if (!ds_is_empty (&e->deferred_text))
+    ds_put_byte (&e->deferred_text, '\n');
+
+  const char *text = text_item_get_text (text_item);
+  ds_put_cstr (&e->deferred_text, text);
+  output_item_unref (item);
 
-  return *classp;
+  return true;
 }
 
-static struct output_driver *
-create_driver (const char *driver_name, const char *class_name,
-               const char *device_type, struct string_map *options)
+/* 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 *class;
-  enum output_device_type type;
-  struct output_driver *driver;
+  struct output_engine *e = engine_stack_top ();
+
+  if (item == NULL)
+    return;
 
-  type = OUTPUT_DEVICE_UNKNOWN;
-  if (device_type != NULL && device_type[0] != '\0')
+  if (defer_text (e, item))
+    return;
+  flush_deferred_text (e);
+
+  if (is_group_open_item (item))
     {
-      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);
+      const struct group_open_item *group_open_item
+        = to_group_open_item (item);
+      if (e->n_groups >= e->allocated_groups)
+        e->groups = x2nrealloc (e->groups, &e->allocated_groups,
+                                sizeof *e->groups);
+      e->groups[e->n_groups] = (group_open_item->command_name
+                                ? xstrdup (group_open_item->command_name)
+                                : NULL);
+      e->n_groups++;
     }
+  else if (is_group_close_item (item))
+    {
+      assert (e->n_groups > 0);
 
-  class = find_output_class (class_name);
-  if (class != NULL)
-    driver = class->create (driver_name, type, options);
-  else
+      size_t idx = --e->n_groups;
+      free (e->groups[idx]);
+      if (idx >= 1 && idx <= 4)
+        {
+          char *key = xasprintf ("Head%zu", idx);
+          free (string_map_find_and_delete (&e->heading_vars, key));
+          free (key);
+        }
+    }
+  else if (is_text_item (item))
     {
-      error (0, 0, _("unknown output driver class `%s'"), class_name);
-      driver = NULL;
+      const struct text_item *text_item = to_text_item (item);
+      enum text_item_type type = text_item_get_type (text_item);
+      const char *text = text_item_get_text (text_item);
+      if (type == TEXT_ITEM_TITLE
+          && e->n_groups >= 1 && e->n_groups <= 4)
+        {
+          char *key = xasprintf ("Head%zu", e->n_groups);
+          string_map_replace (&e->heading_vars, key, text);
+          free (key);
+        }
+      else if (type == TEXT_ITEM_PAGE_TITLE)
+        string_map_replace (&e->heading_vars, "PageTitle", text);
     }
 
-  string_map_destroy (options);
-
-  return driver;
+  output_submit__ (e, item);
 }
 
-struct output_driver *
-output_driver_create (const char *class_name, struct string_map *options)
-{
-  return create_driver (class_name, class_name, NULL, options);
-}
-
-/* String LINE is in format:
-   DRIVERNAME:CLASSNAME:DEVICETYPE:OPTIONS
-*/
-void
-output_configure_driver (const char *line_)
+const char *
+output_get_command_name (void)
 {
-  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));
-
-  if (driver_name && class_name)
+  if (n_stack)
     {
-      struct string_map option_map;
-      struct output_driver *driver;
-
-      string_map_init (&option_map);
-      if (options != NULL)
-        parse_options (driver_name, options, &option_map);
-
-      driver = create_driver (driver_name, class_name,
-                              device_type, &option_map);
-      if (driver != NULL)
-        output_driver_register (driver);
+      struct output_engine *e = engine_stack_top ();
+      for (size_t i = e->n_groups; i-- > 0; )
+        if (e->groups[i])
+          return e->groups[i];
     }
-  else
-    error (0, 0,
-           _("driver definition line missing driver name or class name"));
 
-  free (line);
+  return NULL;
 }
 
-/* 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;
-
-  printf (_("Driver classes:"));
-  for (classp = driver_classes; *classp != NULL; classp++)
-    printf (" %s", (*classp)->name);
-  putc ('\n', stdout);
-}
+  struct output_engine *e = engine_stack_top ();
+  struct llx *llx;
 
-static bool
-driver_is_enabled (const struct output_driver *d)
-{
-  return (1u << d->device_type) & enabled_device_types;
+  flush_deferred_text (e);
+  for (llx = llx_head (&e->drivers); llx != llx_null (&e->drivers);
+       llx = llx_next (llx))
+    {
+      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 = src ? xstrdup (src) : NULL;
+
+  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));
+  text_item_submit (text_item_create_nocopy (TEXT_ITEM_PAGE_TITLE,
+                                             page_title));
 }
 
-/* Submits ITEM to the configured output drivers, and transfers ownership to
-   the output subsystem. */
 void
-output_submit (struct output_item *item)
+output_set_title (const char *title)
 {
-  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;
+  struct output_engine *e = engine_stack_top ();
 
-        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__ (item);
+  output_set_title__ (e, &e->title, 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_subtitle (const char *subtitle)
 {
-  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);
-    }
-}
-
-unsigned int
-output_get_enabled_types (void)
-{
-  return enabled_device_types;
+  output_set_title__ (e, &e->subtitle, subtitle);
 }
 
 void
-output_set_enabled_types (unsigned int types)
+output_set_filename (const char *filename)
 {
-  enabled_device_types = types;
+  struct output_engine *e = engine_stack_top ();
+
+  string_map_replace (&e->heading_vars, "Filename", filename);
 }
 
-void
-output_set_type_enabled (bool enable, enum output_device_type type)
+size_t
+output_get_group_level (void)
 {
-  unsigned int bit = 1u << type;
-  if (enable)
-    enabled_device_types |= bit;
-  else
-    enabled_device_types |= ~bit;
+  struct output_engine *e = engine_stack_top ();
+
+  return e->n_groups;
 }
 \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 +386,235 @@ 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;
+
+  for (e = engine_stack; e < &engine_stack[n_stack]; e++)
+    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;
-
-  for (i = 0; i < n_drivers; i++)
-    if (drivers[i] == driver)
-      {
-        remove_element (drivers, n_drivers, sizeof *drivers, i);
-        return;
-      }
-  NOT_REACHED ();
+  struct output_engine *e = output_driver_get_engine (driver);
+
+  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;
-
-  for (i = 0; i < n_drivers; i++)
-    if (drivers[i] == driver)
-      return true;
-  return false;
+  return output_driver_get_engine (driver) != NULL;
 }
 \f
-/* Known driver classes. */
+extern const struct output_driver_factory txt_driver_factory;
+extern const struct output_driver_factory list_driver_factory;
+extern const struct output_driver_factory html_driver_factory;
+extern const struct output_driver_factory csv_driver_factory;
+extern const struct output_driver_factory odt_driver_factory;
+extern const struct output_driver_factory spv_driver_factory;
+#ifdef HAVE_CAIRO
+extern const struct output_driver_factory pdf_driver_factory;
+extern const struct output_driver_factory ps_driver_factory;
+extern const struct output_driver_factory svg_driver_factory;
+#endif
 
-static const struct output_driver_class *driver_classes[] =
+static const struct output_driver_factory *factories[] =
   {
-    &ascii_class,
+    &txt_driver_factory,
+    &list_driver_factory,
+    &html_driver_factory,
+    &csv_driver_factory,
+    &odt_driver_factory,
+    &spv_driver_factory,
 #ifdef HAVE_CAIRO
-    &cairo_class,
+    &pdf_driver_factory,
+    &ps_driver_factory,
+    &svg_driver_factory,
 #endif
-    &html_class,
-    &odt_class,
-    &csv_class,
-    NULL,
+    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
+/* Extracts the actual text content from the given Pango MARKUP and returns it
+   as as a malloc()'d string. */
+char *
+output_get_text_from_markup (const char *markup)
+{
+  xmlParserCtxt *parser = xmlCreatePushParserCtxt (NULL, NULL, NULL, 0, NULL);
+  if (!parser)
+    return xstrdup (markup);
+
+  xmlParseChunk (parser, "<xml>", strlen ("<xml>"), false);
+  xmlParseChunk (parser, markup, strlen (markup), false);
+  xmlParseChunk (parser, "</xml>", strlen ("</xml>"), true);
+
+  char *content
+    = (parser->wellFormed
+       ? CHAR_CAST (char *,
+                    xmlNodeGetContent (xmlDocGetRootElement (parser->myDoc)))
+       : xstrdup (markup));
+  xmlFreeDoc (parser->myDoc);
+  xmlFreeParserCtxt (parser);
+
+  return content;
+}
+
+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);
+}