pivot-table: Implement hiding footnotes.
[pspp] / src / output / tex.c
1 /* PSPP - a program for statistical analysis.
2    Copyright (C) 2020 Free Software Foundation, Inc.
3
4    This program is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13
14    You should have received a copy of the GNU General Public License
15    along with this program.  If not, see <http://www.gnu.org/licenses/>. */
16
17 #include <config.h>
18
19 #include <errno.h>
20 #include <stdint.h>
21 #include <stdlib.h>
22 #include <ctype.h>
23 #include <time.h>
24 #include <unistd.h>
25 #include <locale.h>
26
27 #include "gl/mbiter.h"
28 #include "data/file-name.h"
29 #include "data/file-handle-def.h"
30 #include "libpspp/assertion.h"
31 #include "libpspp/cast.h"
32 #include "libpspp/compiler.h"
33 #include "libpspp/hmap.h"
34 #include "libpspp/ll.h"
35 #include "libpspp/i18n.h"
36 #include "libpspp/message.h"
37 #include "libpspp/temp-file.h"
38 #include "libpspp/version.h"
39 #include "output/cairo-chart.h"
40 #include "output/chart-item.h"
41 #include "output/driver-provider.h"
42 #include "output/image-item.h"
43 #include "output/message-item.h"
44 #include "output/options.h"
45 #include "output/output-item-provider.h"
46 #include "output/pivot-output.h"
47 #include "output/pivot-table.h"
48 #include "output/table-provider.h"
49 #include "output/table-item.h"
50 #include "output/text-item.h"
51 #include "output/tex-rendering.h"
52 #include "output/tex-parsing.h"
53
54
55 #include "tex-glyphs.h"
56
57 #include "gl/minmax.h"
58 #include "gl/xalloc.h"
59 #include "gl/c-vasnprintf.h"
60
61 #include "gettext.h"
62 #define _(msgid) gettext (msgid)
63
64 /* This file uses TABLE_HORZ and TABLE_VERT enough to warrant abbreviating. */
65 #define H TABLE_HORZ
66 #define V TABLE_VERT
67
68 /* The desired maximum line length in the TeX file.  */
69 #define TEX_LINE_MAX 80
70
71 struct tex_driver
72   {
73     struct output_driver driver;
74     /* A hash table containing any Tex macros which need to be emitted.  */
75     struct hmap macros;
76     bool require_graphics;
77     struct cell_color fg;
78     struct cell_color bg;
79     struct file_handle *handle;
80     char *chart_file_name;
81
82     FILE *file;
83     size_t chart_cnt;
84
85     struct ll_list preamble_list;
86     struct ll_list token_list;
87   };
88
89 /* Ships the string STR to the driver.  */
90 static void
91 shipout (struct ll_list *list, const char *str, ...)
92 {
93   va_list args;
94   va_start (args, str);
95
96   size_t length;
97   char *s = c_vasnprintf (NULL, &length, str, args);
98
99   tex_parse (s, list);
100
101   va_end (args);
102   free (s);
103 }
104
105 static const struct output_driver_class tex_driver_class;
106
107 static void tex_output_table (struct tex_driver *, const struct table_item *);
108
109 static struct tex_driver *
110 tex_driver_cast (struct output_driver *driver)
111 {
112   assert (driver->class == &tex_driver_class);
113   return UP_CAST (driver, struct tex_driver, driver);
114 }
115
116 static struct driver_option *
117 opt (struct output_driver *d, struct string_map *options, const char *key,
118      const char *default_value)
119 {
120   return driver_option_get (d, options, key, default_value);
121 }
122
123 static struct output_driver *
124 tex_create (struct file_handle *fh, enum settings_output_devices device_type,
125              struct string_map *o)
126 {
127   struct output_driver *d;
128   struct tex_driver *tex = XZALLOC (struct tex_driver);
129   hmap_init (&tex->macros);
130   ll_init (&tex->preamble_list);
131   ll_init (&tex->token_list);
132
133   d = &tex->driver;
134   output_driver_init (&tex->driver, &tex_driver_class, fh_get_file_name (fh),
135                       device_type);
136   tex->handle = fh;
137   tex->chart_file_name = parse_chart_file_name (opt (d, o, "charts",
138                                                       fh_get_file_name (fh)));
139   tex->chart_cnt = 1;
140   tex->bg = parse_color (opt (d, o, "background-color", "#FFFFFFFFFFFF"));
141   tex->fg = parse_color (opt (d, o, "foreground-color", "#000000000000"));
142
143   tex->file = fn_open (tex->handle, "w");
144   if (tex->file == NULL)
145     {
146       msg_error (errno, _("error opening output file `%s'"),
147                  fh_get_file_name (tex->handle));
148       goto error;
149     }
150
151   return d;
152
153  error:
154   output_driver_destroy (d);
155   return NULL;
156 }
157
158
159 /* Emit all the tokens in LIST to FILE.
160    Then destroy LIST and its contents.  */
161 static void
162 post_process_tokens (FILE *file, struct ll_list *list)
163 {
164   size_t line_len = 0;
165   struct tex_token *tt;
166   struct tex_token *ttnext;
167   ll_for_each_safe (tt, ttnext, struct tex_token, ll, list)
168     {
169       if (tt->cat == CAT_SPACE)
170         {
171           /* Count the number of characters up to the next space,
172              and if it'll not fit on to the line, then make a line
173              break here.  */
174           size_t word_len = 0;
175           struct tex_token *prev_x = NULL;
176           for (struct ll *x = ll_next (&tt->ll); x != ll_null (list);
177                x = ll_next (x))
178             {
179               struct tex_token *nt = ll_data (x, struct tex_token, ll);
180               if (nt->cat == CAT_SPACE || nt->cat == CAT_EOL)
181                 break;
182               if (prev_x && (prev_x->cat == CAT_COMMENT) && (nt->cat != CAT_COMMENT))
183                 break;
184               word_len += ds_length (&nt->str);
185               prev_x = nt;
186             }
187
188           if ((word_len < TEX_LINE_MAX) && (line_len + word_len >= TEX_LINE_MAX - 1))
189             {
190               fputs ("\n", file);
191               line_len = 0;
192               continue;
193             }
194         }
195
196       line_len += ds_length (&tt->str);
197       if (tt->cat == CAT_EOL)
198         line_len = 0;
199       if (line_len >= TEX_LINE_MAX)
200         {
201           fputs ("%\n", file);
202           line_len = ds_length (&tt->str);
203         }
204       if (tt->cat == CAT_COMMENT)
205         line_len = 0;
206       fputs (ds_cstr (&tt->str), file);
207       ds_destroy (&tt->str);
208       free (tt);
209     }
210 }
211
212
213 static void
214 tex_destroy (struct output_driver *driver)
215 {
216   struct tex_driver *tex = tex_driver_cast (driver);
217
218   shipout (&tex->preamble_list, "%%%% TeX output of pspp\n\n");
219   shipout (&tex->preamble_list, "%%%% Define the horizontal space between table columns\n");
220   shipout (&tex->preamble_list, "\\def\\psppcolumnspace{1mm}\n\n");
221
222   char *ln = get_language ();
223   if (ln)
224     shipout (&tex->preamble_list, "%%%% Language is \"%s\"\n", ln);
225   free (ln);
226   shipout (&tex->preamble_list, "\n");
227
228   shipout (&tex->preamble_list, "%%%% Sets the environment for rendering material in table cell\n");
229   shipout (&tex->preamble_list, "%%%% The parameter is the number of columns in the table\n");
230   shipout (&tex->preamble_list,
231            "\\def\\cell#1{\\normalbaselines\\advance\\hsize by -#1.0\\psppcolumnspace"
232            "\\advance\\hsize by \\psppcolumnspace"
233            "\\divide\\hsize by #1"
234            "\\noindent\\raggedright\\hskip0pt}\n\n");
235
236   /* centre macro */
237   shipout (&tex->preamble_list,
238            "%%%% Render the text centre justified\n"
239            "\\def\\startcentre{\\begingroup\\leftskip=0pt plus 1fil\n"
240            "\\rightskip=\\leftskip\\parfillskip=0pt}\n");
241   shipout (&tex->preamble_list, "\\def\\stopcentre{\\par\\endgroup}\n");
242   shipout (&tex->preamble_list, "\\long\\def\\centre#1{\\startcentre#1\\stopcentre}\n\n");
243
244
245   /* right macro */
246   shipout (&tex->preamble_list,
247            "%%%% Render the text right justified\n"
248            "\\def\\startright{\\begingroup\\leftskip=0pt plus 1fil\n"
249            "\\parfillskip=0pt}\n");
250   shipout (&tex->preamble_list, "\\def\\stopright{\\par\\endgroup}\n");
251   shipout (&tex->preamble_list, "\\long\\def\\right#1{\\startright#1\\stopright}\n\n");
252
253
254   /* Emit all the macro defintions.  */
255   struct tex_macro *m;
256   struct tex_macro *next;
257   HMAP_FOR_EACH_SAFE (m, next, struct tex_macro, node, &tex->macros)
258     {
259       shipout (&tex->preamble_list, "%s", tex_macro[m->index]);
260       shipout (&tex->preamble_list, "\n\n");
261       free (m);
262     }
263   hmap_destroy (&tex->macros);
264
265   if (tex->require_graphics)
266     shipout (&tex->preamble_list, "\\input graphicx\n\n");
267
268   post_process_tokens (tex->file, &tex->preamble_list);
269
270   shipout (&tex->token_list, "\n\\bye\n");
271
272   post_process_tokens (tex->file, &tex->token_list);
273
274   fn_close (tex->handle, tex->file);
275
276   free (tex->chart_file_name);
277   fh_unref (tex->handle);
278   free (tex);
279 }
280
281 /* Ship out TEXT (which must be a UTF-8 encoded string to the driver's output.
282    if TABULAR is true, then this text is within a table.  */
283 static void
284 tex_escape_string (struct tex_driver *tex, const char *text,
285                    bool tabular)
286 {
287   size_t n = strlen (text);
288   while (n > 0)
289     {
290       const char *frag = u8_to_tex_fragments (&text, &n, &tex->macros);
291       shipout (&tex->token_list, "%s", frag);
292       if (text[0] != '\0' && tabular && 0 == strcmp (frag, "."))
293         {
294           /* Peek ahead to the next code sequence */
295           size_t nn = n;
296           const char *t = text;
297           const char *next = u8_to_tex_fragments (&t, &nn, &tex->macros);
298           /* If a period followed by whitespace is encountered within tabular
299              material, then it is reasonable to assume, that it is an
300              abbreviation (like "Sig." or "Std. Deviation") rather than the
301              end of a sentance.  */
302           if (next && 0 == strcmp (" ", next))
303             {
304               shipout (&tex->token_list, "\\ ");
305             }
306         }
307     }
308 }
309
310 static void
311 tex_submit (struct output_driver *driver,
312              const struct output_item *output_item)
313 {
314   struct tex_driver *tex = tex_driver_cast (driver);
315
316   if (is_table_item (output_item))
317     {
318       struct table_item *table_item = to_table_item (output_item);
319       tex_output_table (tex, table_item);
320     }
321   else if (is_image_item (output_item) && tex->chart_file_name != NULL)
322     {
323       struct image_item *image_item = to_image_item (output_item);
324       char *file_name = xr_write_png_image (
325         image_item->image, tex->chart_file_name, tex->chart_cnt++);
326       if (file_name != NULL)
327         {
328           shipout (&tex->token_list, "\\includegraphics{%s}\n", file_name);
329           tex->require_graphics = true;
330           free (file_name);
331         }
332     }
333   else if (is_chart_item (output_item) && tex->chart_file_name != NULL)
334     {
335       struct chart_item *chart_item = to_chart_item (output_item);
336       char *file_name = xr_draw_png_chart (chart_item, tex->chart_file_name,
337                                            tex->chart_cnt++,
338                                            &tex->fg,
339                                            &tex->bg);
340       if (file_name != NULL)
341         {
342           //const char *title = chart_item_get_title (chart_item);
343           //          printf ("The chart title is %s\n", title);
344
345           shipout (&tex->token_list, "\\includegraphics{%s}\n", file_name);
346           tex->require_graphics = true;
347           free (file_name);
348         }
349     }
350   else if (is_text_item (output_item))
351     {
352       struct text_item *text_item = to_text_item (output_item);
353       const char *s = text_item_get_text (text_item);
354
355       switch (text_item_get_type (text_item))
356         {
357         case TEXT_ITEM_PAGE_TITLE:
358           shipout (&tex->token_list, "\\headline={\\bf ");
359           tex_escape_string (tex, s, false);
360           shipout (&tex->token_list, "\\hfil}\n");
361           break;
362
363         case TEXT_ITEM_LOG:
364           shipout (&tex->token_list, "{\\tt ");
365           tex_escape_string (tex, s, false);
366           shipout (&tex->token_list, "}\\par\n\n");
367           break;
368
369         case TEXT_ITEM_SYNTAX:
370           /* So far as I'm aware, this can never happen.  */
371         default:
372           printf ("Unhandled type %d\n", text_item_get_type (text_item));
373           break;
374         }
375     }
376   else if (is_message_item (output_item))
377     {
378       const struct message_item *message_item = to_message_item (output_item);
379       char *s = msg_to_string (message_item_get_msg (message_item));
380       tex_escape_string (tex, s, false);
381       shipout (&tex->token_list, "\\par\n");
382       free (s);
383     }
384 }
385
386 static void
387 tex_put_footnote_markers (struct tex_driver *tex,
388                           const struct pivot_table *pt,
389                           const size_t *footnote_indexes,
390                           size_t n_footnotes)
391 {
392   size_t n_visible = 0;
393   for (size_t i = 0; i < n_footnotes; i++)
394     {
395       const struct pivot_footnote *f = pt->footnotes[footnote_indexes[i]];
396       if (f->show)
397         {
398           if (!n_visible++)
399             shipout (&tex->token_list, "$^{");
400
401           char *marker = pivot_footnote_marker_string (f, pt);
402           tex_escape_string (tex, marker, true);
403           free (marker);
404         }
405     }
406   if (n_visible)
407     shipout (&tex->token_list, "}$");
408 }
409
410 static void
411 tex_put_table_cell (struct tex_driver *tex, const struct pivot_table *pt,
412                     const struct table_cell *cell)
413 {
414   struct string s = DS_EMPTY_INITIALIZER;
415   pivot_value_format_body (cell->value, pt, &s);
416   tex_escape_string (tex, ds_cstr (&s), false);
417   ds_destroy (&s);
418
419   tex_put_footnote_markers (tex, pt,
420                             cell->value->footnote_indexes,
421                             cell->value->n_footnotes);
422 }
423
424 static void
425 tex_output_table_layer (struct tex_driver *tex, const struct pivot_table *pt,
426                         const size_t *layer_indexes)
427 {
428   /* Tables are rendered in TeX with the \halign command.
429      This is described in the TeXbook Ch. 22 */
430   struct table *title, *layers, *body, *caption;
431   struct pivot_footnote **footnotes;
432   size_t n_footnotes;
433   pivot_output (pt, layer_indexes, true, &title, &layers, &body,
434                 &caption, NULL, &footnotes, &n_footnotes);
435
436   shipout (&tex->token_list, "\n{\\parindent=0pt\n");
437
438   if (caption)
439     {
440       shipout (&tex->token_list, "{\\sl ");
441       struct table_cell cell;
442       table_get_cell (caption, 0, 0, &cell);
443       tex_put_table_cell (tex, pt, &cell);
444       shipout (&tex->token_list, "}\n\n");
445     }
446
447   if (title || layers)
448     {
449       if (title)
450         {
451           shipout (&tex->token_list, "{\\bf ");
452           struct table_cell cell;
453           table_get_cell (title, 0, 0, &cell);
454           tex_put_table_cell (tex, pt, &cell);
455           shipout (&tex->token_list, "}\\par\n");
456         }
457
458       if (layers)
459         {
460           for (size_t y = 0; y < layers->n[V]; y++)
461             {
462               shipout (&tex->token_list, "{");
463               struct table_cell cell;
464               table_get_cell (layers, 0, y, &cell);
465               tex_put_table_cell (tex, pt, &cell);
466               shipout (&tex->token_list, "}\\par\n");
467             }
468         }
469     }
470
471   shipout (&tex->token_list, "\\offinterlineskip\\halign{\\strut%%\n");
472
473   /* Generate the preamble */
474   for (int x = 0; x < body->n[H]; ++x)
475     {
476       shipout (&tex->token_list, "{\\vbox{\\cell{%d}#}}", body->n[H]);
477
478       if (x < body->n[H] - 1)
479         {
480           shipout (&tex->token_list, "\\hskip\\psppcolumnspace\\hfil");
481           shipout (&tex->token_list, "&\\vrule\n");
482         }
483       else
484         shipout (&tex->token_list, "\\cr\n");
485     }
486
487   /* Emit the row data */
488   for (int y = 0; y < body->n[V]; y++)
489     {
490       enum { H = TABLE_HORZ, V = TABLE_VERT };
491       bool is_column_header = y < body->h[V][0] || y >= body->n[V] - body->h[V][1];
492       int prev_x = -1;
493       int skipped = 0;
494       for (int x = 0; x < body->n[H];)
495         {
496           struct table_cell cell;
497
498           table_get_cell (body, x, y, &cell);
499
500           int colspan = table_cell_colspan (&cell);
501           if (x > 0)
502             shipout (&tex->token_list, "&");
503           else
504             for (int i = 0; i < skipped - colspan; ++i)
505               shipout (&tex->token_list, "&");
506
507
508           if (x != cell.d[TABLE_HORZ][0] || y != cell.d[TABLE_VERT][0])
509             goto next_1;
510
511           /* bool is_header = (y < body->h[V][0] */
512           /*                   || y >= body->n[V] - body->h[V][1] */
513           /*                   || x < body->h[H][0] */
514           /*                   || x >= body->n[H] - body->h[H][1]); */
515
516           struct string s = DS_EMPTY_INITIALIZER;
517           bool numeric = pivot_value_format_body (cell.value, pt, &s);
518
519           enum table_halign halign = table_halign_interpret (
520             cell.cell_style->halign, numeric);
521
522           /* int rowspan = table_cell_rowspan (&cell); */
523
524           /* if (rowspan > 1) */
525           /*   fprintf (tex->file, " rowspan=\"%d\"", rowspan); */
526
527           if (colspan > 1)
528             {
529               shipout (&tex->token_list, "\\multispan{%d}\\span", colspan - 1);
530               shipout (&tex->token_list, "\\hsize=%d.0\\hsize", colspan);
531               shipout (&tex->token_list, "\\advance\\hsize%d.0\\psppcolumnspace ",
532                        colspan - 1);
533             }
534
535           if (halign == TABLE_HALIGN_CENTER)
536             shipout (&tex->token_list, "\\centre{");
537
538           if (halign == TABLE_HALIGN_RIGHT)
539             shipout (&tex->token_list, "\\right{");
540
541           /* Output cell contents. */
542           tex_escape_string (tex, ds_cstr (&s), true);
543           ds_destroy (&s);
544
545           tex_put_footnote_markers (tex, pt, cell.value->footnote_indexes,
546                                     cell.value->n_footnotes);
547           if (halign == TABLE_HALIGN_CENTER || halign == TABLE_HALIGN_RIGHT)
548             {
549               shipout (&tex->token_list, "}");
550             }
551
552         next_1:
553           skipped = x - prev_x;
554           prev_x = x;
555           x = cell.d[TABLE_HORZ][1];
556         }
557       shipout (&tex->token_list, "\\cr\n");
558       if (is_column_header)
559         shipout (&tex->token_list, "\\noalign{\\hrule\\vskip -\\normalbaselineskip}\\cr\n");
560     }
561
562   shipout (&tex->token_list, "}%% End of \\halign\n");
563
564   /* Shipout any footnotes.  */
565   if (n_footnotes > 0)
566     shipout (&tex->token_list, "\\vskip 0.5ex\n");
567
568   for (int i = 0; i < n_footnotes; ++i)
569     {
570       char *marker = pivot_footnote_marker_string (footnotes[i], pt);
571       char *content = pivot_value_to_string (footnotes[i]->content, pt);
572
573       shipout (&tex->token_list, "$^{");
574       tex_escape_string (tex, marker, false);
575       shipout (&tex->token_list, "}$");
576       tex_escape_string (tex, content, false);
577
578       free (content);
579       free (marker);
580     }
581
582   shipout (&tex->token_list, "}\n\\vskip 3ex\n\n");
583
584   table_unref (title);
585   table_unref (layers);
586   table_unref (body);
587   table_unref (caption);
588   free (footnotes);
589 }
590
591 static void
592 tex_output_table (struct tex_driver *tex, const struct table_item *item)
593 {
594   size_t *layer_indexes;
595   PIVOT_OUTPUT_FOR_EACH_LAYER (layer_indexes, item->pt, true)
596     tex_output_table_layer (tex, item->pt, layer_indexes);
597 }
598
599 struct output_driver_factory tex_driver_factory =
600   { "tex", "pspp.tex", tex_create };
601
602 static const struct output_driver_class tex_driver_class =
603   {
604     "tex",
605     tex_destroy,
606     tex_submit,
607     NULL,
608   };