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