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