render: Simplify 'pages' array in struct render_pager.
[pspp] / src / output / table.c
index 5dc5651fd940c0b86c5d9d6d38d5e5408fb2646f..44c33b9d10b36ceabb6df4125d1038567f16076d 100644 (file)
@@ -1,5 +1,5 @@
 /* PSPP - a program for statistical analysis.
-   Copyright (C) 2009, 2011, 2014 Free Software Foundation, Inc.
+   Copyright (C) 2009, 2011, 2014, 2016 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 "output/table-provider.h"
 
 #include <assert.h>
+#include <inttypes.h>
 #include <stdlib.h>
 
+#include "data/format.h"
+#include "libpspp/assertion.h"
 #include "libpspp/cast.h"
 #include "libpspp/compiler.h"
+#include "libpspp/pool.h"
+#include "libpspp/str.h"
 #include "output/table-item.h"
+#include "output/table.h"
+#include "output/text-item.h"
 
 #include "gl/xalloc.h"
 
+/* This file uses TABLE_HORZ and TABLE_VERT enough to warrant abbreviating. */
+#define H TABLE_HORZ
+#define V TABLE_VERT
+
 /* Increases TABLE's reference count, indicating that it has an additional
    owner.  An table that is shared among multiple owners must not be
    modified. */
@@ -48,7 +59,7 @@ table_unref (struct table *table)
     {
       assert (table->ref_cnt > 0);
       if (--table->ref_cnt == 0)
-        table->klass->destroy (table);
+        pool_destroy (table->container);
     }
 }
 
@@ -59,362 +70,806 @@ table_is_shared (const struct table *table)
 {
   return table->ref_cnt > 1;
 }
+\f
+struct table_area_style *
+table_area_style_clone (struct pool *pool, const struct table_area_style *old)
+{
+  struct table_area_style *new = pool_malloc (pool, sizeof *new);
+  *new = *old;
+  if (new->font_style.typeface)
+    new->font_style.typeface = pool_strdup (pool, new->font_style.typeface);
+  return new;
+}
 
-/* Sets the number of left header columns in TABLE to HL. */
 void
-table_set_hl (struct table *table, int hl)
+table_area_style_free (struct table_area_style *style)
 {
-  assert (!table_is_shared (table));
-  table->h[TABLE_HORZ][0] = hl;
+  if (style)
+    {
+      free (style->font_style.typeface);
+      free (style);
+    }
+}
+
+struct footnote *
+footnote_clone (const struct footnote *old)
+{
+  struct footnote *new = xmalloc (sizeof *new);
+  *new = (struct footnote) {
+    .idx = old->idx,
+    .content = old->content ? xstrdup (old->content) : NULL,
+    .marker = old->marker ? xstrdup (old->marker) : NULL,
+    .style = old->style ? table_area_style_clone (NULL, old->style) : NULL,
+  };
+  return new;
 }
 
-/* Sets the number of right header columns in TABLE to HR. */
 void
-table_set_hr (struct table *table, int hr)
+footnote_destroy (struct footnote *f)
 {
-  assert (!table_is_shared (table));
-  table->h[TABLE_HORZ][1] = hr;
+  if (f)
+    {
+      free (f->content);
+      free (f->marker);
+      if (f->style)
+        {
+          table_area_style_uninit (f->style);
+          free (f->style);
+        }
+      free (f);
+    }
+}
+
+struct table_cell *
+table_cell_clone (const struct table_cell *old)
+{
+  struct table_cell *new = xmalloc (sizeof *new);
+  *new = *old;
+  new->text = xstrdup (new->text);
+
+  if (old->n_subscripts)
+    {
+      new->subscripts = xnmalloc (old->n_subscripts, sizeof *new->subscripts);
+      for (size_t i = 0; i < old->n_subscripts; i++)
+        new->subscripts[i] = xstrdup (old->subscripts[i]);
+    }
+  else
+    new->subscripts = NULL;
+
+  if (old->n_footnotes)
+    {
+      new->footnotes = xnmalloc (old->n_footnotes, sizeof *new->footnotes);
+      for (size_t i = 0; i < old->n_footnotes; i++)
+        new->footnotes[i] = footnote_clone (old->footnotes[i]);
+    }
+  else
+    new->footnotes = NULL;
+
+  if (old->style)
+    new->style = table_area_style_clone (NULL, old->style);
+
+  return new;
 }
 
-/* Sets the number of top header rows in TABLE to HT. */
 void
-table_set_ht (struct table *table, int ht)
+table_cell_destroy (struct table_cell *cell)
 {
-  assert (!table_is_shared (table));
-  table->h[TABLE_VERT][0] = ht;
+  if (!cell)
+    return;
+
+  free (cell->text);
+  for (size_t i = 0; i < cell->n_subscripts; i++)
+    free (cell->subscripts[i]);
+  free (cell->subscripts);
+  for (size_t i = 0; i < cell->n_footnotes; i++)
+    footnote_destroy (cell->footnotes[i]);
+  free (cell->footnotes);
+  if (cell->style)
+    {
+      table_area_style_uninit (cell->style);
+      free (cell->style);
+    }
+  free (cell);
 }
 
-/* Sets the number of top header rows in TABLE to HB. */
 void
-table_set_hb (struct table *table, int hb)
+table_cell_format_footnote_markers (const struct table_cell *cell,
+                                    struct string *s)
 {
-  assert (!table_is_shared (table));
-  table->h[TABLE_VERT][1] = hb;
+  for (size_t i = 0; i < cell->n_footnotes; i++)
+    {
+      if (i)
+        ds_put_byte (s, ',');
+      ds_put_cstr (s, cell->footnotes[i]->marker);
+    }
+}
+
+static struct footnote **
+add_footnotes (struct footnote **refs, size_t n_refs,
+               struct footnote **footnotes, size_t *allocated, size_t *n)
+{
+  for (size_t i = 0; i < n_refs; i++)
+    {
+      struct footnote *f = refs[i];
+      if (f->idx >= *allocated)
+        {
+          size_t new_allocated = (f->idx + 1) * 2;
+          footnotes = xrealloc (footnotes, new_allocated * sizeof *footnotes);
+          while (*allocated < new_allocated)
+            footnotes[(*allocated)++] = NULL;
+        }
+      footnotes[f->idx] = f;
+      if (f->idx >= *n)
+        *n = f->idx + 1;
+    }
+  return footnotes;
+}
+
+size_t
+table_collect_footnotes (const struct table_item *item,
+                         struct footnote ***footnotesp)
+{
+  struct footnote **footnotes = NULL;
+  size_t allocated = 0;
+  size_t n = 0;
+
+  struct table *t = item->table;
+  for (int y = 0; y < t->n[V]; y++)
+    {
+      struct table_cell cell;
+      for (int x = 0; x < t->n[H]; x = cell.d[TABLE_HORZ][1])
+        {
+          table_get_cell (t, x, y, &cell);
+
+          if (x == cell.d[TABLE_HORZ][0] && y == cell.d[TABLE_VERT][0])
+            footnotes = add_footnotes (cell.footnotes, cell.n_footnotes,
+                                       footnotes, &allocated, &n);
+        }
+    }
+
+  const struct table_cell *title = table_item_get_title (item);
+  if (title)
+    footnotes = add_footnotes (title->footnotes, title->n_footnotes,
+                               footnotes, &allocated, &n);
+
+  const struct table_item_layers *layers = table_item_get_layers (item);
+  if (layers)
+    {
+      for (size_t i = 0; i < layers->n_layers; i++)
+        footnotes = add_footnotes (layers->layers[i].footnotes,
+                                   layers->layers[i].n_footnotes,
+                                   footnotes, &allocated, &n);
+    }
+
+  const struct table_cell *caption = table_item_get_caption (item);
+  if (caption)
+    footnotes = add_footnotes (caption->footnotes, caption->n_footnotes,
+                               footnotes, &allocated, &n);
+
+  size_t n_nonnull = 0;
+  for (size_t i = 0; i < n; i++)
+    if (footnotes[i])
+      footnotes[n_nonnull++] = footnotes[i];
+
+  *footnotesp = footnotes;
+  return n_nonnull;
 }
 \f
-/* Initializes TABLE as a table of the specified CLASS, initially with a
-   reference count of 1.
+const char *
+table_halign_to_string (enum table_halign halign)
+{
+  switch (halign)
+    {
+    case TABLE_HALIGN_LEFT: return "left";
+    case TABLE_HALIGN_CENTER: return "center";
+    case TABLE_HALIGN_RIGHT: return "right";
+    case TABLE_HALIGN_DECIMAL: return "decimal";
+    case TABLE_HALIGN_MIXED: return "mixed";
+    default: return "**error**";
+    }
+}
 
-   TABLE initially has 0 rows and columns and no headers.  The table
-   implementation should update the numbers of rows and columns.  The table
-   implementation (or its client) may update the header rows and columns.
+const char *
+table_valign_to_string (enum table_valign valign)
+{
+  switch (valign)
+    {
+    case TABLE_VALIGN_TOP: return "top";
+    case TABLE_VALIGN_CENTER: return "center";
+    case TABLE_VALIGN_BOTTOM: return "bottom";
+    default: return "**error**";
+    }
+}
 
-   A table is an abstract class, that is, a plain struct table is not useful on
-   its own.  Thus, this function is normally called from the initialization
-   function of some subclass of table. */
-void
-table_init (struct table *table, const struct table_class *class)
+enum table_halign
+table_halign_interpret (enum table_halign halign, bool numeric)
 {
-  table->klass = class;
-  table->n[TABLE_HORZ] = table->n[TABLE_VERT] = 0;
-  table->h[TABLE_HORZ][0] = table->h[TABLE_HORZ][1] = 0;
-  table->h[TABLE_VERT][0] = table->h[TABLE_VERT][1] = 0;
-  table->ref_cnt = 1;
+  switch (halign)
+    {
+    case TABLE_HALIGN_LEFT:
+    case TABLE_HALIGN_CENTER:
+    case TABLE_HALIGN_RIGHT:
+      return halign;
+
+    case TABLE_HALIGN_MIXED:
+      return numeric ? TABLE_HALIGN_RIGHT : TABLE_HALIGN_LEFT;
+
+    case TABLE_HALIGN_DECIMAL:
+      return TABLE_HALIGN_DECIMAL;
+
+    default:
+      NOT_REACHED ();
+    }
 }
 
-/* Sets the number of columns in TABLE to NC. */
 void
-table_set_nc (struct table *table, int nc)
+font_style_copy (struct pool *container,
+                 struct font_style *dst, const struct font_style *src)
 {
-  assert (!table_is_shared (table));
-  table->n[TABLE_HORZ] = nc;
+  *dst = *src;
+  if (dst->typeface)
+    dst->typeface = pool_strdup (container, dst->typeface);
 }
 
-/* Sets the number of rows in TABLE to NR. */
 void
-table_set_nr (struct table *table, int nr)
+font_style_uninit (struct font_style *font)
 {
-  assert (!table_is_shared (table));
-  table->n[TABLE_VERT] = nr;
+  if (font)
+    free (font->typeface);
 }
-\f
-/* Initializes CELL with the contents of the table cell at column X and row Y
-   within TABLE.  When CELL is no longer needed, the caller is responsible for
-   freeing it by calling table_cell_free(CELL).
 
-   The caller must ensure that CELL is destroyed before TABLE is unref'ed. */
 void
-table_get_cell (const struct table *table, int x, int y,
-                struct table_cell *cell)
+table_area_style_copy (struct pool *container, struct table_area_style *dst,
+                       const struct table_area_style *src)
 {
-  assert (x >= 0 && x < table->n[TABLE_HORZ]);
-  assert (y >= 0 && y < table->n[TABLE_VERT]);
-  table->klass->get_cell (table, x, y, cell);
+  font_style_copy (container, &dst->font_style, &src->font_style);
+  dst->cell_style = src->cell_style;
 }
 
-/* Frees CELL, which should have been initialized by calling
-   table_get_cell(). */
 void
-table_cell_free (struct table_cell *cell)
+table_area_style_uninit (struct table_area_style *area)
 {
-  if (cell->destructor != NULL)
-    cell->destructor (cell->destructor_aux);
+  if (area)
+    font_style_uninit (&area->font_style);
 }
 
-/* Returns one of the TAL_* enumeration constants (declared in output/table.h)
-   representing a rule running alongside one of the cells in TABLE.
+const char *
+table_stroke_to_string (enum table_stroke stroke)
+{
+  switch (stroke)
+    {
+    case TABLE_STROKE_NONE: return "none";
+    case TABLE_STROKE_SOLID: return "solid";
+    case TABLE_STROKE_DASHED: return "dashed";
+    case TABLE_STROKE_THICK: return "thick";
+    case TABLE_STROKE_THIN: return "thin";
+    case TABLE_STROKE_DOUBLE: return "double";
+    default:
+      return "**error**";
+    }
+}
 
-   Suppose NC is the number of columns in TABLE and NR is the number of rows.
-   Then, if AXIS is TABLE_HORZ, then 0 <= X <= NC and 0 <= Y < NR.  If (X,Y) =
-   (0,0), the return value is the rule that runs vertically on the left side of
-   cell (0,0); if (X,Y) = (1,0), it is the vertical rule between that cell and
-   cell (1,0); and so on, up to (NC,0), which runs vertically on the right of
-   cell (NC-1,0).
+void
+cell_color_dump (const struct cell_color *c)
+{
+  if (c->alpha != 255)
+    printf ("rgba(%d, %d, %d, %d)", c->r, c->g, c->b, c->alpha);
+  else
+    printf ("#%02"PRIx8"%02"PRIx8"%02"PRIx8, c->r, c->g, c->b);
+}
 
-   The following diagram illustrates the meaning of (X,Y) for AXIS = TABLE_HORZ
-   within a 7x7 table.  The '|' characters at the intersection of the X labels
-   and Y labels show the rule whose style would be returned by calling
-   table_get_rule with those X and Y values:
+void
+font_style_dump (const struct font_style *f)
+{
+  printf ("%s %dpx ", f->typeface, f->size);
+  cell_color_dump (&f->fg[0]);
+  putchar ('/');
+  cell_color_dump (&f->bg[0]);
+  if (!cell_color_equal (&f->fg[0], &f->fg[1])
+      || !cell_color_equal (&f->bg[0], &f->bg[1]))
+    {
+      printf (" alt=");
+      cell_color_dump (&f->fg[1]);
+      putchar ('/');
+      cell_color_dump (&f->bg[1]);
+    }
+  if (f->bold)
+    fputs (" bold", stdout);
+  if (f->italic)
+    fputs (" italic", stdout);
+  if (f->underline)
+    fputs (" underline", stdout);
+}
 
-                           0  1  2  3  4  5  6  7
-                           +--+--+--+--+--+--+--+
-                         0 |  |  |  |  |  |  |  |
-                           +--+--+--+--+--+--+--+
-                         1 |  |  |  |  |  |  |  |
-                           +--+--+--+--+--+--+--+
-                         2 |  |  |  |  |  |  |  |
-                           +--+--+--+--+--+--+--+
-                         3 |  |  |  |  |  |  |  |
-                           +--+--+--+--+--+--+--+
-                         4 |  |  |  |  |  |  |  |
-                           +--+--+--+--+--+--+--+
-                         5 |  |  |  |  |  |  |  |
-                           +--+--+--+--+--+--+--+
-                         6 |  |  |  |  |  |  |  |
-                           +--+--+--+--+--+--+--+
+bool
+font_style_equal (const struct font_style *a, const struct font_style *b)
+{
+  return (a->bold == b->bold
+          && a->italic == b->italic
+          && a->underline == b->underline
+          && a->markup == b->markup
+          && cell_color_equal (&a->fg[0], &b->fg[0])
+          && cell_color_equal (&a->fg[1], &b->fg[1])
+          && cell_color_equal (&a->bg[0], &b->bg[0])
+          && cell_color_equal (&a->bg[1], &b->bg[1])
+          && !strcmp (a->typeface ? a->typeface : "",
+                      b->typeface ? b->typeface : "")
+          && a->size == b->size);
+}
 
-   Similarly, if AXIS is TABLE_VERT, then 0 <= X < NC and 0 <= Y <= NR.  If
-   (X,Y) = (0,0), the return value is the rule that runs horizontally above
-   the top of cell (0,0); if (X,Y) = (0,1), it is the horizontal rule
-   between that cell and cell (0,1); and so on, up to (0,NR), which runs
-   horizontally below cell (0,NR-1). */
-int
-table_get_rule (const struct table *table, enum table_axis axis, int x, int y)
+void
+cell_style_dump (const struct cell_style *c)
 {
-  assert (x >= 0 && x < table->n[TABLE_HORZ] + (axis == TABLE_HORZ));
-  assert (y >= 0 && y < table->n[TABLE_VERT] + (axis == TABLE_VERT));
-  return table->klass->get_rule (table, axis, x, y);
+  fputs (table_halign_to_string (c->halign), stdout);
+  if (c->halign == TABLE_HALIGN_DECIMAL)
+    printf ("(%.2gpx)", c->decimal_offset);
+  printf (" %s", table_valign_to_string (c->valign));
+  printf (" %d,%d,%d,%dpx",
+          c->margin[TABLE_HORZ][0], c->margin[TABLE_HORZ][1],
+          c->margin[TABLE_VERT][0], c->margin[TABLE_VERT][1]);
 }
 \f
-struct table_unshared
-  {
-    struct table table;
-    struct table *subtable;
-  };
 
-static const struct table_class table_unshared_class;
+static const bool debugging = true;
 
-/* Takes ownership of TABLE and returns a table with the same contents but
-   which is guaranteed not to be shared (as returned by table_is_shared()).
+/* Creates and returns a new table with NC columns and NR rows and initially no
+   header rows or columns.
 
-   If TABLE is unshared, just returns TABLE.
+   Sets the number of header rows on each side of TABLE to HL on the
+   left, HR on the right, HT on the top, HB on the bottom.  Header rows
+   are repeated when a table is broken across multiple columns or
+   multiple pages.
 
-   The only real use for this function is to create a copy of TABLE in which
-   the headers can be adjusted, which is a pretty specialized use case. */
+   The table's cells are initially empty. */
 struct table *
-table_unshare (struct table *table)
+table_create (int nc, int nr, int hl, int hr, int ht, int hb)
 {
-  if (!table_is_shared (table))
-    return table;
-  else
+  struct table *t;
+
+  t = pool_create_container (struct table, container);
+  t->n[TABLE_HORZ] = nc;
+  t->n[TABLE_VERT] = nr;
+  t->h[TABLE_HORZ][0] = hl;
+  t->h[TABLE_HORZ][1] = hr;
+  t->h[TABLE_VERT][0] = ht;
+  t->h[TABLE_VERT][1] = hb;
+  t->ref_cnt = 1;
+
+  t->cc = pool_calloc (t->container, nr * nc, sizeof *t->cc);
+  t->ct = pool_calloc (t->container, nr * nc, sizeof *t->ct);
+
+  t->rh = pool_nmalloc (t->container, nc, nr + 1);
+  memset (t->rh, TABLE_STROKE_NONE, nc * (nr + 1));
+
+  t->rv = pool_nmalloc (t->container, nr, nc + 1);
+  memset (t->rv, TABLE_STROKE_NONE, nr * (nc + 1));
+
+  memset (t->styles, 0, sizeof t->styles);
+  memset (t->rule_colors, 0, sizeof t->rule_colors);
+
+  return t;
+}
+\f
+/* Rules. */
+
+/* Draws a vertical line to the left of cells at horizontal position X
+   from Y1 to Y2 inclusive in style STYLE, if style is not -1. */
+void
+table_vline (struct table *t, int style, int x, int y1, int y2)
+{
+  if (debugging)
+    {
+      if (x < 0 || x > t->n[H]
+          || y1 < 0 || y1 >= t->n[V]
+          || y2 < 0 || y2 >= t->n[V])
+        {
+          printf ("bad vline: x=%d y=(%d,%d) in table size (%d,%d)\n",
+                  x, y1, y2, t->n[H], t->n[V]);
+          return;
+        }
+    }
+
+  assert (x >= 0);
+  assert (x <= t->n[H]);
+  assert (y1 >= 0);
+  assert (y2 >= y1);
+  assert (y2 <= t->n[V]);
+
+  if (style != -1)
     {
-      struct table_unshared *tiu = xmalloc (sizeof *tiu);
-      table_init (&tiu->table, &table_unshared_class);
-      table_set_nc (&tiu->table, table_nc (table));
-      table_set_nr (&tiu->table, table_nr (table));
-      table_set_hl (&tiu->table, table_hl (table));
-      table_set_hr (&tiu->table, table_hr (table));
-      table_set_ht (&tiu->table, table_ht (table));
-      table_set_hb (&tiu->table, table_hb (table));
-      tiu->subtable = table;
-      return &tiu->table;
+      int y;
+      for (y = y1; y <= y2; y++)
+        t->rv[x + (t->n[H] + 1) * y] = style;
     }
 }
 
-static struct table_unshared *
-table_unshared_cast (const struct table *table)
+/* Draws a horizontal line above cells at vertical position Y from X1
+   to X2 inclusive in style STYLE, if style is not -1. */
+void
+table_hline (struct table *t, int style, int x1, int x2, int y)
 {
-  assert (table->klass == &table_unshared_class);
-  return UP_CAST (table, struct table_unshared, table);
+  if (debugging)
+    {
+      if (y < 0 || y > t->n[V]
+          || x1 < 0 || x1 >= t->n[H]
+          || x2 < 0 || x2 >= t->n[H])
+        {
+          printf ("bad hline: x=(%d,%d) y=%d in table size (%d,%d)\n",
+                  x1, x2, y, t->n[H], t->n[V]);
+          return;
+        }
+    }
+
+  assert (y >= 0);
+  assert (y <= t->n[V]);
+  assert (x2 >= x1);
+  assert (x1 >= 0);
+  assert (x2 < t->n[H]);
+
+  if (style != -1)
+    {
+      int x;
+      for (x = x1; x <= x2; x++)
+        t->rh[x + t->n[H] * y] = style;
+    }
 }
 
-static void
-table_unshared_destroy (struct table *tiu_)
+/* Draws a box around cells (X1,Y1)-(X2,Y2) inclusive with horizontal
+   lines of style F_H and vertical lines of style F_V.  Fills the
+   interior of the box with horizontal lines of style I_H and vertical
+   lines of style I_V.  Any of the line styles may be -1 to avoid
+   drawing those lines.  This is distinct from 0, which draws a null
+   line. */
+void
+table_box (struct table *t, int f_h, int f_v, int i_h, int i_v,
+           int x1, int y1, int x2, int y2)
 {
-  struct table_unshared *tiu = table_unshared_cast (tiu_);
-  table_unref (tiu->subtable);
-  free (tiu);
+  if (debugging)
+    {
+      if (x1 < 0 || x1 >= t->n[H]
+          || x2 < 0 || x2 >= t->n[H]
+          || y1 < 0 || y1 >= t->n[V]
+          || y2 < 0 || y2 >= t->n[V])
+        {
+          printf ("bad box: (%d,%d)-(%d,%d) in table size (%d,%d)\n",
+                  x1, y1, x2, y2, t->n[H], t->n[V]);
+          NOT_REACHED ();
+        }
+    }
+
+  assert (x2 >= x1);
+  assert (y2 >= y1);
+  assert (x1 >= 0);
+  assert (y1 >= 0);
+  assert (x2 < t->n[H]);
+  assert (y2 < t->n[V]);
+
+  if (f_h != -1)
+    {
+      int x;
+      for (x = x1; x <= x2; x++)
+        {
+          t->rh[x + t->n[H] * y1] = f_h;
+          t->rh[x + t->n[H] * (y2 + 1)] = f_h;
+        }
+    }
+  if (f_v != -1)
+    {
+      int y;
+      for (y = y1; y <= y2; y++)
+        {
+          t->rv[x1 + (t->n[H] + 1) * y] = f_v;
+          t->rv[(x2 + 1) + (t->n[H] + 1) * y] = f_v;
+        }
+    }
+
+  if (i_h != -1)
+    {
+      int y;
+
+      for (y = y1 + 1; y <= y2; y++)
+        {
+          int x;
+
+          for (x = x1; x <= x2; x++)
+            t->rh[x + t->n[H] * y] = i_h;
+        }
+    }
+  if (i_v != -1)
+    {
+      int x;
+
+      for (x = x1 + 1; x <= x2; x++)
+        {
+          int y;
+
+          for (y = y1; y <= y2; y++)
+            t->rv[x + (t->n[H] + 1) * y] = i_v;
+        }
+    }
 }
+\f
+/* Cells. */
 
 static void
-table_unshared_get_cell (const struct table *tiu_, int x, int y,
-                              struct table_cell *cell)
+do_table_text (struct table *table, int c, int r, unsigned opt, char *text)
 {
-  struct table_unshared *tiu = table_unshared_cast (tiu_);
-  table_get_cell (tiu->subtable, x, y, cell);
+  assert (c >= 0);
+  assert (r >= 0);
+  assert (c < table->n[H]);
+  assert (r < table->n[V]);
+
+  if (debugging)
+    {
+      if (c < 0 || r < 0 || c >= table->n[H] || r >= table->n[V])
+        {
+          printf ("table_text(): bad cell (%d,%d) in table size (%d,%d)\n",
+                  c, r, table->n[H], table->n[V]);
+          return;
+        }
+    }
+
+  table->cc[c + r * table->n[H]] = text;
+  table->ct[c + r * table->n[H]] = opt;
 }
 
-static int
-table_unshared_get_rule (const struct table *tiu_,
-                              enum table_axis axis, int x, int y)
+/* Sets cell (C,R) in TABLE, with options OPT, to have text value
+   TEXT. */
+void
+table_text (struct table *table, int c, int r, unsigned opt,
+          const char *text)
 {
-  struct table_unshared *tiu = table_unshared_cast (tiu_);
-  return table_get_rule (tiu->subtable, axis, x, y);
+  do_table_text (table, c, r, opt, pool_strdup (table->container, text));
 }
 
-static const struct table_class table_unshared_class =
-  {
-    table_unshared_destroy,
-    table_unshared_get_cell,
-    table_unshared_get_rule,
-    NULL,                       /* paste */
-    NULL,                       /* select */
-  };
-\f
-struct table_string
-  {
-    struct table table;
-    char *string;
-    unsigned int options;
-  };
+/* Sets cell (C,R) in TABLE, with options OPT, to have text value
+   FORMAT, which is formatted as if passed to printf. */
+void
+table_text_format (struct table *table, int c, int r, unsigned opt,
+                   const char *format, ...)
+{
+  va_list args;
 
-static const struct table_class table_string_class;
+  va_start (args, format);
+  do_table_text (table, c, r, opt,
+                 pool_vasprintf (table->container, format, args));
+  va_end (args);
+}
 
-/* Returns a table that contains a single cell, whose contents are S with
-   options OPTIONS (a combination of TAB_* values).  */
-struct table *
-table_from_string (unsigned int options, const char *s)
+static struct table_cell *
+add_joined_cell (struct table *table, int x1, int y1, int x2, int y2,
+                 unsigned opt)
 {
-  struct table_string *ts = xmalloc (sizeof *ts);
-  table_init (&ts->table, &table_string_class);
-  ts->table.n[TABLE_HORZ] = ts->table.n[TABLE_VERT] = 1;
-  ts->string = xstrdup (s);
-  ts->options = options;
-  return &ts->table;
+  assert (x1 >= 0);
+  assert (y1 >= 0);
+  assert (y2 >= y1);
+  assert (x2 >= x1);
+  assert (y2 < table->n[V]);
+  assert (x2 < table->n[H]);
+
+  if (debugging)
+    {
+      if (x1 < 0 || x1 >= table->n[H]
+          || y1 < 0 || y1 >= table->n[V]
+          || x2 < x1 || x2 >= table->n[H]
+          || y2 < y1 || y2 >= table->n[V])
+        {
+          printf ("table_joint_text(): bad cell "
+                  "(%d,%d)-(%d,%d) in table size (%d,%d)\n",
+                  x1, y1, x2, y2, table->n[H], table->n[V]);
+          return NULL;
+        }
+    }
+
+  table_box (table, -1, -1, TABLE_STROKE_NONE, TABLE_STROKE_NONE,
+             x1, y1, x2, y2);
+
+  struct table_cell *cell = pool_alloc (table->container, sizeof *cell);
+  *cell = (struct table_cell) {
+    .d = { [TABLE_HORZ] = { x1, ++x2 },
+           [TABLE_VERT] = { y1, ++y2 } },
+    .options = opt,
+  };
+
+  void **cc = &table->cc[x1 + y1 * table->n[H]];
+  unsigned short *ct = &table->ct[x1 + y1 * table->n[H]];
+  const int ofs = table->n[H] - (x2 - x1);
+  for (int y = y1; y < y2; y++)
+    {
+      for (int x = x1; x < x2; x++)
+        {
+          *cc++ = cell;
+          *ct++ = opt | TAB_JOIN;
+        }
+
+      cc += ofs;
+      ct += ofs;
+    }
+
+  return cell;
 }
 
-static struct table_string *
-table_string_cast (const struct table *table)
+/* Joins cells (X1,X2)-(Y1,Y2) inclusive in TABLE, and sets them with
+   options OPT to have text value TEXT. */
+void
+table_joint_text (struct table *table, int x1, int y1, int x2, int y2,
+                  unsigned opt, const char *text)
 {
-  assert (table->klass == &table_string_class);
-  return UP_CAST (table, struct table_string, table);
+  char *s = pool_strdup (table->container, text);
+  if (x1 == x2 && y1 == y2)
+    do_table_text (table, x1, y1, opt, s);
+  else
+    add_joined_cell (table, x1, y1, x2, y2, opt)->text = s;
 }
 
-static void
-table_string_destroy (struct table *ts_)
+static struct table_cell *
+get_joined_cell (struct table *table, int x, int y)
 {
-  struct table_string *ts = table_string_cast (ts_);
-  free (ts->string);
-  free (ts);
+  int index = x + y * table->n[H];
+  unsigned short opt = table->ct[index];
+  struct table_cell *cell;
+
+  if (opt & TAB_JOIN)
+    cell = table->cc[index];
+  else
+    {
+      char *text = table->cc[index];
+
+      cell = add_joined_cell (table, x, y, x, y, table->ct[index]);
+      cell->text = text ? text : pool_strdup (table->container, "");
+    }
+  return cell;
 }
 
-static void
-table_string_get_cell (const struct table *ts_, int x UNUSED, int y UNUSED,
-                       struct table_cell *cell)
+/* Sets the subscripts for column X, row Y in TABLE. */
+void
+table_add_subscripts (struct table *table, int x, int y,
+                      char **subscripts, size_t n_subscripts)
 {
-  struct table_string *ts = table_string_cast (ts_);
-  cell->d[TABLE_HORZ][0] = 0;
-  cell->d[TABLE_HORZ][1] = 1;
-  cell->d[TABLE_VERT][0] = 0;
-  cell->d[TABLE_VERT][1] = 1;
-  cell->contents = &cell->inline_contents;
-  cell->inline_contents.options = ts->options;
-  cell->inline_contents.text = ts->string;
-  cell->inline_contents.table = NULL;
-  cell->n_contents = 1;
-  cell->destructor = NULL;
+  struct table_cell *cell = get_joined_cell (table, x, y);
+
+  cell->n_subscripts = n_subscripts;
+  cell->subscripts = pool_nalloc (table->container, n_subscripts,
+                                  sizeof *cell->subscripts);
+  for (size_t i = 0; i < n_subscripts; i++)
+    cell->subscripts[i] = pool_strdup (table->container, subscripts[i]);
 }
 
+/* Create a footnote in TABLE with MARKER (e.g. "a") as its marker and CONTENT
+   as its content.  The footnote will be styled as STYLE, which is mandatory.
+   IDX must uniquely identify the footnote within TABLE.
 
-static int
-table_string_get_rule (const struct table *ts UNUSED,
-                       enum table_axis axis UNUSED, int x UNUSED, int y UNUSED)
+   Returns the new footnote.  The return value is the only way to get to the
+   footnote later, so it is important for the caller to remember it. */
+struct footnote *
+table_create_footnote (struct table *table, size_t idx, const char *content,
+                       const char *marker, struct table_area_style *style)
 {
-  return TAL_0;
+  assert (style);
+
+  struct footnote *f = pool_alloc (table->container, sizeof *f);
+  f->idx = idx;
+  f->content = pool_strdup (table->container, content);
+  f->marker = pool_strdup (table->container, marker);
+  f->style = style;
+  return f;
 }
 
-static const struct table_class table_string_class =
-  {
-    table_string_destroy,
-    table_string_get_cell,
-    table_string_get_rule,
-    NULL,                       /* paste */
-    NULL,                       /* select */
-  };
-\f
-struct table_nested
-  {
-    struct table table;
-    struct table_item *inner;
-  };
+/* Attaches a reference to footnote F to the cell at column X, row Y in
+   TABLE. */
+void
+table_add_footnote (struct table *table, int x, int y, struct footnote *f)
+{
+  assert (f->style);
 
-static const struct table_class table_nested_class;
+  struct table_cell *cell = get_joined_cell (table, x, y);
 
-/* Creates and returns a table with a single cell that contains INNER.
-   Takes ownership of INNER. */
-struct table *
-table_create_nested (struct table *inner)
-{
-  return table_create_nested_item (table_item_create (inner, NULL));
+  cell->footnotes = pool_realloc (
+    table->container, cell->footnotes,
+    (cell->n_footnotes + 1) * sizeof *cell->footnotes);
+
+  cell->footnotes[cell->n_footnotes++] = f;
 }
 
-/* Creates and returns a table with a single cell that contains INNER.
-   Takes ownership of INNER. */
-struct table *
-table_create_nested_item (struct table_item *inner)
+/* Overrides the style for column X, row Y in TABLE with STYLE.
+   Does not make a copy of STYLE, so it should either be allocated from
+   TABLE->container or have a lifetime that will outlive TABLE. */
+void
+table_add_style (struct table *table, int x, int y,
+                 struct table_area_style *style)
 {
-  struct table_nested *tn = xmalloc (sizeof *tn);
-  table_init (&tn->table, &table_nested_class);
-  tn->table.n[TABLE_HORZ] = tn->table.n[TABLE_VERT] = 1;
-  tn->inner = table_item_ref (inner);
-  return &tn->table;
+  get_joined_cell (table, x, y)->style = style;
 }
 
-static struct table_nested *
-table_nested_cast (const struct table *table)
+/* Returns true if column C, row R has no contents, otherwise false. */
+bool
+table_cell_is_empty (const struct table *table, int c, int r)
 {
-  assert (table->klass == &table_nested_class);
-  return UP_CAST (table, struct table_nested, table);
+  return table->cc[c + r * table->n[H]] == NULL;
 }
-
-static void
-table_nested_destroy (struct table *tn_)
+\f
+/* Initializes CELL with the contents of the table cell at column X and row Y
+   within TABLE. */
+void
+table_get_cell (const struct table *t, int x, int y, struct table_cell *cell)
 {
-  struct table_nested *tn = table_nested_cast (tn_);
-  table_item_unref (tn->inner);
-  free (tn);
+  assert (x >= 0 && x < t->n[TABLE_HORZ]);
+  assert (y >= 0 && y < t->n[TABLE_VERT]);
+
+  int index = x + y * t->n[H];
+  unsigned short opt = t->ct[index];
+  const void *cc = t->cc[index];
+
+  struct table_area_style *style
+    = t->styles[(opt & TAB_STYLE_MASK) >> TAB_STYLE_SHIFT];
+  if (opt & TAB_JOIN)
+    {
+      const struct table_cell *jc = cc;
+      *cell = *jc;
+      if (!cell->style)
+        cell->style = style;
+    }
+  else
+    *cell = (struct table_cell) {
+      .d = { [TABLE_HORZ] = { x, x + 1 },
+             [TABLE_VERT] = { y, y + 1 } },
+      .options = opt,
+      .text = CONST_CAST (char *, cc ? cc : ""),
+      .style = style,
+    };
+
+  assert (cell->style);
 }
 
-static void
-table_nested_get_cell (const struct table *tn_, int x UNUSED, int y UNUSED,
-                       struct table_cell *cell)
-{
-  struct table_nested *tn = table_nested_cast (tn_);
-  cell->d[TABLE_HORZ][0] = 0;
-  cell->d[TABLE_HORZ][1] = 1;
-  cell->d[TABLE_VERT][0] = 0;
-  cell->d[TABLE_VERT][1] = 1;
-  cell->contents = &cell->inline_contents;
-  cell->inline_contents.options = TAB_LEFT;
-  cell->inline_contents.text = NULL;
-  cell->inline_contents.table = tn->inner;
-  cell->n_contents = 1;
-  cell->destructor = NULL;
-}
-
-static int
-table_nested_get_rule (const struct table *tn UNUSED,
-                       enum table_axis axis UNUSED, int x UNUSED, int y UNUSED)
-{
-  return TAL_0;
-}
-
-static const struct table_class table_nested_class =
-  {
-    table_nested_destroy,
-    table_nested_get_cell,
-    table_nested_get_rule,
-    NULL,                       /* paste */
-    NULL,                       /* select */
-  };
+/* Returns one of the TAL_* enumeration constants (declared in output/table.h)
+   representing a rule running alongside one of the cells in TABLE.
+
+   Suppose NC is the number of columns in TABLE and NR is the number of rows.
+   Then, if AXIS is TABLE_HORZ, then 0 <= X <= NC and 0 <= Y < NR.  If (X,Y) =
+   (0,0), the return value is the rule that runs vertically on the left side of
+   cell (0,0); if (X,Y) = (1,0), it is the vertical rule between that cell and
+   cell (1,0); and so on, up to (NC,0), which runs vertically on the right of
+   cell (NC-1,0).
+
+   The following diagram illustrates the meaning of (X,Y) for AXIS = TABLE_HORZ
+   within a 7x7 table.  The '|' characters at the intersection of the X labels
+   and Y labels show the rule whose style would be returned by calling
+   table_get_rule with those X and Y values:
+
+                           0  1  2  3  4  5  6  7
+                           +--+--+--+--+--+--+--+
+                         0 |  |  |  |  |  |  |  |
+                           +--+--+--+--+--+--+--+
+                         1 |  |  |  |  |  |  |  |
+                           +--+--+--+--+--+--+--+
+                         2 |  |  |  |  |  |  |  |
+                           +--+--+--+--+--+--+--+
+                         3 |  |  |  |  |  |  |  |
+                           +--+--+--+--+--+--+--+
+                         4 |  |  |  |  |  |  |  |
+                           +--+--+--+--+--+--+--+
+                         5 |  |  |  |  |  |  |  |
+                           +--+--+--+--+--+--+--+
+                         6 |  |  |  |  |  |  |  |
+                           +--+--+--+--+--+--+--+
+
+   Similarly, if AXIS is TABLE_VERT, then 0 <= X < NC and 0 <= Y <= NR.  If
+   (X,Y) = (0,0), the return value is the rule that runs horizontally above
+   the top of cell (0,0); if (X,Y) = (0,1), it is the horizontal rule
+   between that cell and cell (0,1); and so on, up to (0,NR), which runs
+   horizontally below cell (0,NR-1). */
+int
+table_get_rule (const struct table *table, enum table_axis axis, int x, int y,
+                struct cell_color *color)
+{
+  assert (x >= 0 && x < table->n[TABLE_HORZ] + (axis == TABLE_HORZ));
+  assert (y >= 0 && y < table->n[TABLE_VERT] + (axis == TABLE_VERT));
 
+  uint8_t raw = (axis == TABLE_VERT
+                 ? table->rh[x + table->n[H] * y]
+                 : table->rv[x + (table->n[H] + 1) * y]);
+  struct cell_color *p = table->rule_colors[(raw & TAB_RULE_STYLE_MASK)
+                                            >> TAB_RULE_STYLE_SHIFT];
+  *color = p ? *p : (struct cell_color) CELL_COLOR_BLACK;
+  return (raw & TAB_RULE_TYPE_MASK) >> TAB_RULE_TYPE_SHIFT;
+}