pivot-table: Implement hiding footnotes.
[pspp] / src / output / html.c
1 /* PSPP - a program for statistical analysis.
2    Copyright (C) 1997-9, 2000, 2009, 2010, 2011, 2012, 2013, 2014, 2017,
3    2020 Free Software Foundation, Inc.
4
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9
10    This program is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU General Public License for more details.
14
15    You should have received a copy of the GNU General Public License
16    along with this program.  If not, see <http://www.gnu.org/licenses/>. */
17
18 #include <config.h>
19
20 #include <errno.h>
21 #include <stdint.h>
22 #include <stdlib.h>
23 #include <ctype.h>
24 #include <time.h>
25 #include <unistd.h>
26 #include <locale.h>
27
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/i18n.h"
34 #include "libpspp/message.h"
35 #include "libpspp/version.h"
36 #include "output/cairo-chart.h"
37 #include "output/chart-item.h"
38 #include "output/driver-provider.h"
39 #include "output/image-item.h"
40 #include "output/message-item.h"
41 #include "output/options.h"
42 #include "output/output-item-provider.h"
43 #include "output/pivot-output.h"
44 #include "output/pivot-table.h"
45 #include "output/table-provider.h"
46 #include "output/table-item.h"
47 #include "output/text-item.h"
48
49 #include "gl/minmax.h"
50 #include "gl/xalloc.h"
51
52 #include "gettext.h"
53 #define _(msgid) gettext (msgid)
54
55 /* This file uses TABLE_HORZ and TABLE_VERT enough to warrant abbreviating. */
56 #define H TABLE_HORZ
57 #define V TABLE_VERT
58
59 struct html_driver
60   {
61     struct output_driver driver;
62     struct cell_color fg;
63     struct cell_color bg;
64     struct file_handle *handle;
65     char *chart_file_name;
66
67     FILE *file;
68     size_t chart_cnt;
69
70     bool bare;
71     bool css;
72     bool borders;
73   };
74
75 static const struct output_driver_class html_driver_class;
76
77 static void html_output_table (struct html_driver *, const struct table_item *);
78 static void escape_string (FILE *file, const char *text,
79                            const char *space, const char *newline);
80 static void print_title_tag (FILE *file, const char *name,
81                              const char *content);
82
83 static struct html_driver *
84 html_driver_cast (struct output_driver *driver)
85 {
86   assert (driver->class == &html_driver_class);
87   return UP_CAST (driver, struct html_driver, driver);
88 }
89
90 static struct driver_option *
91 opt (struct output_driver *d, struct string_map *options, const char *key,
92      const char *default_value)
93 {
94   return driver_option_get (d, options, key, default_value);
95 }
96
97 static void
98 put_header (struct html_driver *html)
99 {
100   fputs ("<!doctype html>\n", html->file);
101   fprintf (html->file, "<html");
102   char *ln = get_language ();
103   if (ln)
104     fprintf (html->file, " lang=\"%s\"", ln);
105   free (ln);
106   fprintf (html->file, ">\n");
107   fputs ("<head>\n", html->file);
108   print_title_tag (html->file, "title", _("PSPP Output"));
109   fprintf (html->file, "<meta name=\"generator\" content=\"%s\">\n", version);
110   fputs ("<meta http-equiv=\"content-type\" "
111          "content=\"text/html; charset=utf-8\">\n", html->file);
112
113   if (html->css)
114     {
115       fputs ("<style>\n"
116              "<!--\n"
117              "body {\n"
118              "  background: white;\n"
119              "  color: black;\n"
120              "  padding: 0em 12em 0em 3em;\n"
121              "  margin: 0\n"
122              "}\n"
123              "body>p {\n"
124              "  margin: 0pt 0pt 0pt 0em\n"
125              "}\n"
126              "body>p + p {\n"
127              "  text-indent: 1.5em;\n"
128              "}\n"
129              "h1 {\n"
130              "  font-size: 150%;\n"
131              "  margin-left: -1.33em\n"
132              "}\n"
133              "h2 {\n"
134              "  font-size: 125%;\n"
135              "  font-weight: bold;\n"
136              "  margin-left: -.8em\n"
137              "}\n"
138              "h3 {\n"
139              "  font-size: 100%;\n"
140              "  font-weight: bold;\n"
141              "  margin-left: -.5em }\n"
142              "h4 {\n"
143              "  font-size: 100%;\n"
144              "  margin-left: 0em\n"
145              "}\n"
146              "h1, h2, h3, h4, h5, h6 {\n"
147              "  font-family: sans-serif;\n"
148              "  color: blue\n"
149              "}\n"
150              "html {\n"
151              "  margin: 0\n"
152              "}\n"
153              "code {\n"
154              "  font-family: sans-serif\n"
155              "}\n"
156              "table {\n"
157              "  border-collapse: collapse;\n"
158              "  margin-bottom: 1em\n"
159              "}\n"
160              "caption {\n"
161              "  text-align: left\n"
162              "}\n"
163              "th { font-weight: normal }\n"
164              "a:link {\n"
165              "  color: #1f00ff;\n"
166              "}\n"
167              "a:visited {\n"
168              "  color: #9900dd;\n"
169              "}\n"
170              "a:active {\n"
171              "  color: red;\n"
172              "}\n"
173              "-->\n"
174              "</style>\n",
175              html->file);
176     }
177   fputs ("</head>\n", html->file);
178   fputs ("<body>\n", html->file);
179 }
180
181 static struct output_driver *
182 html_create (struct file_handle *fh, enum settings_output_devices device_type,
183              struct string_map *o)
184 {
185   struct output_driver *d;
186   struct html_driver *html;
187
188   html = xzalloc (sizeof *html);
189   d = &html->driver;
190   output_driver_init (&html->driver, &html_driver_class, fh_get_file_name (fh),
191                       device_type);
192   html->bare = parse_boolean (opt (d, o, "bare", "false"));
193   html->css = parse_boolean (opt (d, o, "css", "true"));
194   html->borders = parse_boolean (opt (d, o, "borders", "true"));
195
196   html->handle = fh;
197   html->chart_file_name = parse_chart_file_name (opt (d, o, "charts",
198                                                       fh_get_file_name (fh)));
199   html->file = NULL;
200   html->chart_cnt = 1;
201   html->bg = parse_color (opt (d, o, "background-color", "#FFFFFFFFFFFF"));
202   html->fg = parse_color (opt (d, o, "foreground-color", "#000000000000"));
203   html->file = fn_open (html->handle, "w");
204   if (html->file == NULL)
205     {
206       msg_error (errno, _("error opening output file `%s'"), fh_get_file_name (html->handle));
207       goto error;
208     }
209
210   if (!html->bare)
211     put_header (html);
212
213   return d;
214
215  error:
216   output_driver_destroy (d);
217   return NULL;
218 }
219
220 /* Emits <NAME>CONTENT</NAME> to the output, escaping CONTENT as
221    necessary for HTML. */
222 static void
223 print_title_tag (FILE *file, const char *name, const char *content)
224 {
225   if (content != NULL)
226     {
227        fprintf (file, "<%s>", name);
228       escape_string (file, content, " ", " - ");
229       fprintf (file, "</%s>\n", name);
230     }
231 }
232
233 static void
234 html_destroy (struct output_driver *driver)
235 {
236   struct html_driver *html = html_driver_cast (driver);
237
238   if (html->file != NULL)
239     {
240       if (!html->bare)
241         fprintf (html->file,
242                  "</body>\n"
243                  "</html>\n"
244                  "<!-- end of file -->\n");
245       fn_close (html->handle, html->file);
246     }
247   free (html->chart_file_name);
248   fh_unref (html->handle);
249   free (html);
250 }
251
252 static void
253 html_submit (struct output_driver *driver,
254              const struct output_item *output_item)
255 {
256   struct html_driver *html = html_driver_cast (driver);
257
258   if (is_table_item (output_item))
259     {
260       struct table_item *table_item = to_table_item (output_item);
261       html_output_table (html, table_item);
262     }
263   else if (is_image_item (output_item) && html->chart_file_name != NULL)
264     {
265       struct image_item *image_item = to_image_item (output_item);
266       char *file_name = xr_write_png_image (
267         image_item->image, html->chart_file_name, ++html->chart_cnt);
268       if (file_name != NULL)
269         {
270           fprintf (html->file, "<img src=\"%s\">", file_name);
271           free (file_name);
272         }
273     }
274   else if (is_chart_item (output_item) && html->chart_file_name != NULL)
275     {
276       struct chart_item *chart_item = to_chart_item (output_item);
277       char *file_name;
278
279       file_name = xr_draw_png_chart (chart_item, html->chart_file_name,
280                                      html->chart_cnt++,
281                                      &html->fg,
282                                      &html->bg
283                                 );
284       if (file_name != NULL)
285         {
286           const char *title = chart_item_get_title (chart_item);
287           fprintf (html->file, "<img src=\"%s\" alt=\"chart: %s\">",
288                    file_name, title ? title : _("No description"));
289           free (file_name);
290         }
291     }
292   else if (is_text_item (output_item))
293     {
294       struct text_item *text_item = to_text_item (output_item);
295       const char *s = text_item_get_text (text_item);
296
297       switch (text_item_get_type (text_item))
298         {
299         case TEXT_ITEM_PAGE_TITLE:
300           break;
301
302         case TEXT_ITEM_TITLE:
303           {
304             int level = MIN (5, output_get_group_level ()) + 1;
305             char tag[3] = { 'H', level + '1', '\0' };
306             print_title_tag (html->file, tag, s);
307           }
308           break;
309
310         case TEXT_ITEM_SYNTAX:
311           fprintf (html->file, "<pre class=\"syntax\">");
312           escape_string (html->file, s, " ", "<br>");
313           fprintf (html->file, "</pre>\n");
314           break;
315
316         case TEXT_ITEM_LOG:
317           fprintf (html->file, "<p>");
318           escape_string (html->file, s, " ", "<br>");
319           fprintf (html->file, "</p>\n");
320           break;
321         }
322     }
323   else if (is_message_item (output_item))
324     {
325       const struct message_item *message_item = to_message_item (output_item);
326       char *s = msg_to_string (message_item_get_msg (message_item));
327       fprintf (html->file, "<p>");
328       escape_string (html->file, s, " ", "<br>");
329       fprintf (html->file, "</p>\n");
330       free (s);
331     }
332 }
333
334 /* Write TEXT to file F, escaping characters as necessary for HTML.  Spaces are
335    replaced by SPACE, which should be " " or "&nbsp;" New-lines are replaced by
336    NEWLINE, which might be "<BR>" or "\n" or something else appropriate. */
337 static void
338 escape_string (FILE *file, const char *text,
339                const char *space, const char *newline)
340 {
341   for (;;)
342     {
343       char c = *text++;
344       switch (c)
345         {
346         case 0:
347           return;
348         case '\n':
349           fputs (newline, file);
350           break;
351         case '&':
352           fputs ("&amp;", file);
353           break;
354         case '<':
355           fputs ("&lt;", file);
356           break;
357         case '>':
358           fputs ("&gt;", file);
359           break;
360         case ' ':
361           fputs (space, file);
362           break;
363         case '"':
364           fputs ("&quot;", file);
365           break;
366         default:
367           putc (c, file);
368           break;
369         }
370     }
371 }
372
373 static const char *
374 border_to_css (int border)
375 {
376   switch (border)
377     {
378     case TABLE_STROKE_NONE:
379       return NULL;
380
381     case TABLE_STROKE_SOLID:
382       return "solid";
383
384     case TABLE_STROKE_DASHED:
385       return "dashed";
386
387     case TABLE_STROKE_THICK:
388       return "thick solid";
389
390     case TABLE_STROKE_THIN:
391       return "thin solid";
392
393     case TABLE_STROKE_DOUBLE:
394       return "double";
395
396     default:
397       return NULL;
398     }
399
400 }
401
402 struct css_style
403 {
404   FILE *file;
405   int n_styles;
406 };
407
408 static void
409 style_start (struct css_style *cs, FILE *file)
410 {
411   *cs = (struct css_style) {
412     .file = file,
413     .n_styles = 0,
414   };
415 }
416
417 static void
418 style_end (struct css_style *cs)
419 {
420   if (cs->n_styles > 0)
421     fputs ("'", cs->file);
422 }
423
424 static void
425 next_style (struct css_style *st)
426 {
427   bool first = !st->n_styles++;
428   fputs (first ? " style='" : "; ", st->file);
429 }
430
431 static void
432 put_style (struct css_style *st, const char *name, const char *value)
433 {
434   next_style (st);
435   fprintf (st->file, "%s: %s", name, value);
436 }
437
438 static bool
439 format_color (const struct cell_color color,
440               const struct cell_color default_color,
441               char *buf, size_t bufsize)
442 {
443   bool retval = !cell_color_equal (&color, &default_color);
444   if (retval)
445     {
446       if (color.alpha == 255)
447         snprintf (buf, bufsize, "#%02x%02x%02x", color.r, color.g, color.b);
448       else
449         snprintf (buf, bufsize, "rgba(%d, %d, %d, %.3f)",
450                   color.r, color.g, color.b, color.alpha / 255.0);
451     }
452   return retval;
453 }
454
455 static void
456 put_border (const struct table *table, const struct table_cell *cell,
457             struct css_style *style,
458             enum table_axis axis, int h, int v,
459             const char *border_name)
460 {
461   struct cell_color color;
462   const char *css = border_to_css (
463     table_get_rule (table, axis, cell->d[H][h], cell->d[V][v], &color));
464   if (css)
465     {
466       next_style (style);
467       fprintf (style->file, "border-%s: %s", border_name, css);
468
469       char buf[32];
470       if (format_color (color, (struct cell_color) CELL_COLOR_BLACK,
471                         buf, sizeof buf))
472         fprintf (style->file, " %s", buf);
473     }
474 }
475
476 static void
477 html_put_table_cell (struct html_driver *html, const struct pivot_table *pt,
478                      const struct table_cell *cell,
479                      const char *tag, const struct table *t)
480 {
481   fprintf (html->file, "<%s", tag);
482
483   struct css_style style;
484   style_start (&style, html->file);
485
486   struct string body = DS_EMPTY_INITIALIZER;
487   bool numeric = pivot_value_format_body (cell->value, pt, &body);
488
489   enum table_halign halign = table_halign_interpret (cell->cell_style->halign,
490                                                      numeric);
491
492   switch (halign)
493     {
494     case TABLE_HALIGN_RIGHT:
495       put_style (&style, "text-align", "right");
496       break;
497     case TABLE_HALIGN_CENTER:
498       put_style (&style, "text-align", "center");
499       break;
500     default:
501       /* Do nothing */
502       break;
503     }
504
505   if (cell->options & TAB_ROTATE)
506     put_style (&style, "writing-mode", "sideways-lr");
507
508   if (cell->cell_style->valign != TABLE_VALIGN_TOP)
509     {
510       put_style (&style, "vertical-align",
511                  (cell->cell_style->valign == TABLE_VALIGN_BOTTOM
512                   ? "bottom" : "middle"));
513     }
514
515   const struct font_style *fs = cell->font_style;
516   char bgcolor[32];
517   if (format_color (fs->bg[cell->d[V][0] % 2],
518                     (struct cell_color) CELL_COLOR_WHITE,
519                     bgcolor, sizeof bgcolor))
520     put_style (&style, "background", bgcolor);
521
522   char fgcolor[32];
523   if (format_color (fs->fg[cell->d[V][0] % 2],
524                     (struct cell_color) CELL_COLOR_BLACK,
525                     fgcolor, sizeof fgcolor))
526     put_style (&style, "color", fgcolor);
527
528   if (fs->typeface)
529     {
530       put_style (&style, "font-family", "\"");
531       escape_string (html->file, fs->typeface, " ", "\n");
532       putc ('"', html->file);
533     }
534   if (fs->bold)
535     put_style (&style, "font-weight", "bold");
536   if (fs->italic)
537     put_style (&style, "font-style", "italic");
538   if (fs->underline)
539     put_style (&style, "text-decoration", "underline");
540   if (fs->size)
541     {
542       char buf[32];
543       snprintf (buf, sizeof buf, "%dpt", fs->size);
544       put_style (&style, "font-size", buf);
545     }
546
547   if (t && html->borders)
548     {
549       put_border (t, cell, &style, V, 0, 0, "top");
550       put_border (t, cell, &style, H, 0, 0, "left");
551
552       if (cell->d[V][1] == t->n[V])
553         put_border (t, cell, &style, V, 0, 1, "bottom");
554       if (cell->d[H][1] == t->n[H])
555         put_border (t, cell, &style, H, 1, 0, "right");
556     }
557   style_end (&style);
558
559   int colspan = table_cell_colspan (cell);
560   if (colspan > 1)
561     fprintf (html->file, " colspan=\"%d\"", colspan);
562
563   int rowspan = table_cell_rowspan (cell);
564   if (rowspan > 1)
565     fprintf (html->file, " rowspan=\"%d\"", rowspan);
566
567   putc ('>', html->file);
568
569   const char *s = ds_cstr (&body);
570   s += strspn (s, CC_SPACES);
571   escape_string (html->file, s, " ", "<br>");
572   ds_destroy (&body);
573
574   if (cell->value->n_subscripts)
575     {
576       fputs ("<sub>", html->file);
577       for (size_t i = 0; i < cell->value->n_subscripts; i++)
578         {
579           if (i)
580             putc (',', html->file);
581           escape_string (html->file, cell->value->subscripts[i],
582                          "&nbsp;", "<br>");
583         }
584       fputs ("</sub>", html->file);
585     }
586   if (cell->value->n_footnotes > 0)
587     {
588       fputs ("<sup>", html->file);
589       size_t n_footnotes = 0;
590       for (size_t i = 0; i < cell->value->n_footnotes; i++)
591         {
592           const struct pivot_footnote *f
593             = pt->footnotes[cell->value->footnote_indexes[i]];
594           if (f->show)
595             {
596               if (n_footnotes++ > 0)
597                 putc (',', html->file);
598
599               char *marker = pivot_footnote_marker_string (f, pt);
600               escape_string (html->file, marker, " ", "<br>");
601               free (marker);
602             }
603         }
604       fputs ("</sup>", html->file);
605     }
606
607   /* output </th> or </td>. */
608   fprintf (html->file, "</%s>\n", tag);
609 }
610
611 static void
612 html_output_table_layer (struct html_driver *html, const struct pivot_table *pt,
613                          const size_t *layer_indexes)
614 {
615   struct table *title, *layers, *body, *caption, *footnotes;
616   pivot_output (pt, layer_indexes, true, &title, &layers, &body,
617                 &caption, &footnotes, NULL, NULL);
618
619   fputs ("<table", html->file);
620   if (pt->notes)
621     {
622       fputs (" title=\"", html->file);
623       escape_string (html->file, pt->notes, " ", "\n");
624       putc ('"', html->file);
625     }
626   fputs (">\n", html->file);
627
628   if (title)
629     {
630       struct table_cell cell;
631       table_get_cell (title, 0, 0, &cell);
632       html_put_table_cell (html, pt, &cell, "caption", NULL);
633     }
634
635   if (layers)
636     {
637       fputs ("<thead>\n", html->file);
638       for (size_t y = 0; y < layers->n[V]; y++)
639         {
640           fputs ("<tr>\n", html->file);
641
642           struct table_cell cell;
643           table_get_cell (layers, 0, y, &cell);
644           cell.d[H][1] = body->n[H];
645           html_put_table_cell (html, pt, &cell, "td", NULL);
646
647           fputs ("</tr>\n", html->file);
648         }
649       fputs ("</thead>\n", html->file);
650     }
651
652   fputs ("<tbody>\n", html->file);
653   for (int y = 0; y < body->n[V]; y++)
654     {
655       fputs ("<tr>\n", html->file);
656       for (int x = 0; x < body->n[H]; )
657         {
658           struct table_cell cell;
659           table_get_cell (body, x, y, &cell);
660           if (x == cell.d[TABLE_HORZ][0] && y == cell.d[TABLE_VERT][0])
661             {
662               bool is_header = (y < body->h[V][0]
663                                 || y >= body->n[V] - body->h[V][1]
664                                 || x < body->h[H][0]
665                                 || x >= body->n[H] - body->h[H][1]);
666               const char *tag = is_header ? "th" : "td";
667               html_put_table_cell (html, pt, &cell, tag, body);
668             }
669
670           x = cell.d[TABLE_HORZ][1];
671         }
672       fputs ("</tr>\n", html->file);
673     }
674   fputs ("</tbody>\n", html->file);
675
676   if (caption || footnotes)
677     {
678       fprintf (html->file, "<tfoot>\n");
679
680       if (caption)
681         {
682           fputs ("<tr>\n", html->file);
683
684           struct table_cell cell;
685           table_get_cell (caption, 0, 0, &cell);
686           cell.d[H][1] = body->n[H];
687           html_put_table_cell (html, pt, &cell, "td", NULL);
688
689           fputs ("</tr>\n", html->file);
690         }
691
692       if (footnotes)
693         for (size_t y = 0; y < footnotes->n[V]; y++)
694           {
695             fputs ("<tr>\n", html->file);
696
697             struct table_cell cell;
698             table_get_cell (footnotes, 0, y, &cell);
699             cell.d[H][1] = body->n[H];
700             html_put_table_cell (html, pt, &cell, "td", NULL);
701
702             fputs ("</tr>\n", html->file);
703           }
704       fputs ("</tfoot>\n", html->file);
705     }
706
707   fputs ("</table>\n\n", html->file);
708
709   table_unref (title);
710   table_unref (layers);
711   table_unref (body);
712   table_unref (caption);
713   table_unref (footnotes);
714 }
715
716 static void
717 html_output_table (struct html_driver *html, const struct table_item *item)
718 {
719   size_t *layer_indexes;
720   PIVOT_OUTPUT_FOR_EACH_LAYER (layer_indexes, item->pt, true)
721     html_output_table_layer (html, item->pt, layer_indexes);
722 }
723
724 struct output_driver_factory html_driver_factory =
725   { "html", "pspp.html", html_create };
726
727 static const struct output_driver_class html_driver_class =
728   {
729     "html",
730     html_destroy,
731     html_submit,
732     NULL,
733   };