0be011d12876acbac0f55c255aece5c8cb8d9227
[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       char *s = text_item_get_plain_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       free (s);
324     }
325   else if (is_message_item (output_item))
326     {
327       const struct message_item *message_item = to_message_item (output_item);
328       char *s = msg_to_string (message_item_get_msg (message_item));
329       fprintf (html->file, "<p>");
330       escape_string (html->file, s, " ", "<br>");
331       fprintf (html->file, "</p>\n");
332       free (s);
333     }
334 }
335
336 /* Write TEXT to file F, escaping characters as necessary for HTML.  Spaces are
337    replaced by SPACE, which should be " " or "&nbsp;" New-lines are replaced by
338    NEWLINE, which might be "<BR>" or "\n" or something else appropriate. */
339 static void
340 escape_string (FILE *file, const char *text,
341                const char *space, const char *newline)
342 {
343   for (;;)
344     {
345       char c = *text++;
346       switch (c)
347         {
348         case 0:
349           return;
350         case '\n':
351           fputs (newline, file);
352           break;
353         case '&':
354           fputs ("&amp;", file);
355           break;
356         case '<':
357           fputs ("&lt;", file);
358           break;
359         case '>':
360           fputs ("&gt;", file);
361           break;
362         case ' ':
363           fputs (space, file);
364           break;
365         case '"':
366           fputs ("&quot;", file);
367           break;
368         default:
369           putc (c, file);
370           break;
371         }
372     }
373 }
374
375 static const char *
376 border_to_css (int border)
377 {
378   switch (border)
379     {
380     case TABLE_STROKE_NONE:
381       return NULL;
382
383     case TABLE_STROKE_SOLID:
384       return "solid";
385
386     case TABLE_STROKE_DASHED:
387       return "dashed";
388
389     case TABLE_STROKE_THICK:
390       return "thick solid";
391
392     case TABLE_STROKE_THIN:
393       return "thin solid";
394
395     case TABLE_STROKE_DOUBLE:
396       return "double";
397
398     default:
399       return NULL;
400     }
401
402 }
403
404 struct css_style
405 {
406   FILE *file;
407   int n_styles;
408 };
409
410 static void
411 style_start (struct css_style *cs, FILE *file)
412 {
413   *cs = (struct css_style) {
414     .file = file,
415     .n_styles = 0,
416   };
417 }
418
419 static void
420 style_end (struct css_style *cs)
421 {
422   if (cs->n_styles > 0)
423     fputs ("'", cs->file);
424 }
425
426 static void
427 next_style (struct css_style *st)
428 {
429   bool first = !st->n_styles++;
430   fputs (first ? " style='" : "; ", st->file);
431 }
432
433 static void
434 put_style (struct css_style *st, const char *name, const char *value)
435 {
436   next_style (st);
437   fprintf (st->file, "%s: %s", name, value);
438 }
439
440 static bool
441 format_color (const struct cell_color color,
442               const struct cell_color default_color,
443               char *buf, size_t bufsize)
444 {
445   bool retval = !cell_color_equal (&color, &default_color);
446   if (retval)
447     {
448       if (color.alpha == 255)
449         snprintf (buf, bufsize, "#%02x%02x%02x", color.r, color.g, color.b);
450       else
451         snprintf (buf, bufsize, "rgba(%d, %d, %d, %.3f)",
452                   color.r, color.g, color.b, color.alpha / 255.0);
453     }
454   return retval;
455 }
456
457 static void
458 put_border (const struct table *table, const struct table_cell *cell,
459             struct css_style *style,
460             enum table_axis axis, int h, int v,
461             const char *border_name)
462 {
463   struct cell_color color;
464   const char *css = border_to_css (
465     table_get_rule (table, axis, cell->d[H][h], cell->d[V][v], &color));
466   if (css)
467     {
468       next_style (style);
469       fprintf (style->file, "border-%s: %s", border_name, css);
470
471       char buf[32];
472       if (format_color (color, (struct cell_color) CELL_COLOR_BLACK,
473                         buf, sizeof buf))
474         fprintf (style->file, " %s", buf);
475     }
476 }
477
478 static void
479 html_put_table_cell (struct html_driver *html, const struct pivot_table *pt,
480                      const struct table_cell *cell,
481                      const char *tag, const struct table *t)
482 {
483   fprintf (html->file, "<%s", tag);
484
485   struct css_style style;
486   style_start (&style, html->file);
487
488   struct string body = DS_EMPTY_INITIALIZER;
489   bool numeric = pivot_value_format_body (cell->value, pt, &body);
490
491   enum table_halign halign = table_halign_interpret (cell->cell_style->halign,
492                                                      numeric);
493
494   switch (halign)
495     {
496     case TABLE_HALIGN_RIGHT:
497       put_style (&style, "text-align", "right");
498       break;
499     case TABLE_HALIGN_CENTER:
500       put_style (&style, "text-align", "center");
501       break;
502     default:
503       /* Do nothing */
504       break;
505     }
506
507   if (cell->options & TAB_ROTATE)
508     put_style (&style, "writing-mode", "sideways-lr");
509
510   if (cell->cell_style->valign != TABLE_VALIGN_TOP)
511     {
512       put_style (&style, "vertical-align",
513                  (cell->cell_style->valign == TABLE_VALIGN_BOTTOM
514                   ? "bottom" : "middle"));
515     }
516
517   const struct font_style *fs = cell->font_style;
518   char bgcolor[32];
519   if (format_color (fs->bg[cell->d[V][0] % 2],
520                     (struct cell_color) CELL_COLOR_WHITE,
521                     bgcolor, sizeof bgcolor))
522     put_style (&style, "background", bgcolor);
523
524   char fgcolor[32];
525   if (format_color (fs->fg[cell->d[V][0] % 2],
526                     (struct cell_color) CELL_COLOR_BLACK,
527                     fgcolor, sizeof fgcolor))
528     put_style (&style, "color", fgcolor);
529
530   if (fs->typeface)
531     {
532       put_style (&style, "font-family", "\"");
533       escape_string (html->file, fs->typeface, " ", "\n");
534       putc ('"', html->file);
535     }
536   if (fs->bold)
537     put_style (&style, "font-weight", "bold");
538   if (fs->italic)
539     put_style (&style, "font-style", "italic");
540   if (fs->underline)
541     put_style (&style, "text-decoration", "underline");
542   if (fs->size)
543     {
544       char buf[32];
545       snprintf (buf, sizeof buf, "%dpt", fs->size);
546       put_style (&style, "font-size", buf);
547     }
548
549   if (t && html->borders)
550     {
551       put_border (t, cell, &style, V, 0, 0, "top");
552       put_border (t, cell, &style, H, 0, 0, "left");
553
554       if (cell->d[V][1] == t->n[V])
555         put_border (t, cell, &style, V, 0, 1, "bottom");
556       if (cell->d[H][1] == t->n[H])
557         put_border (t, cell, &style, H, 1, 0, "right");
558     }
559   style_end (&style);
560
561   int colspan = table_cell_colspan (cell);
562   if (colspan > 1)
563     fprintf (html->file, " colspan=\"%d\"", colspan);
564
565   int rowspan = table_cell_rowspan (cell);
566   if (rowspan > 1)
567     fprintf (html->file, " rowspan=\"%d\"", rowspan);
568
569   putc ('>', html->file);
570
571   const char *s = ds_cstr (&body);
572   s += strspn (s, CC_SPACES);
573   escape_string (html->file, s, " ", "<br>");
574   ds_destroy (&body);
575
576   if (cell->value->n_subscripts)
577     {
578       fputs ("<sub>", html->file);
579       for (size_t i = 0; i < cell->value->n_subscripts; i++)
580         {
581           if (i)
582             putc (',', html->file);
583           escape_string (html->file, cell->value->subscripts[i],
584                          "&nbsp;", "<br>");
585         }
586       fputs ("</sub>", html->file);
587     }
588   if (cell->value->n_footnotes > 0)
589     {
590       fputs ("<sup>", html->file);
591       size_t n_footnotes = 0;
592       for (size_t i = 0; i < cell->value->n_footnotes; i++)
593         {
594           const struct pivot_footnote *f
595             = pt->footnotes[cell->value->footnote_indexes[i]];
596           if (f->show)
597             {
598               if (n_footnotes++ > 0)
599                 putc (',', html->file);
600
601               char *marker = pivot_footnote_marker_string (f, pt);
602               escape_string (html->file, marker, " ", "<br>");
603               free (marker);
604             }
605         }
606       fputs ("</sup>", html->file);
607     }
608
609   /* output </th> or </td>. */
610   fprintf (html->file, "</%s>\n", tag);
611 }
612
613 static void
614 html_output_table_layer (struct html_driver *html, const struct pivot_table *pt,
615                          const size_t *layer_indexes)
616 {
617   struct table *title, *layers, *body, *caption, *footnotes;
618   pivot_output (pt, layer_indexes, true, &title, &layers, &body,
619                 &caption, &footnotes, NULL, NULL);
620
621   fputs ("<table", html->file);
622   if (pt->notes)
623     {
624       fputs (" title=\"", html->file);
625       escape_string (html->file, pt->notes, " ", "\n");
626       putc ('"', html->file);
627     }
628   fputs (">\n", html->file);
629
630   if (title)
631     {
632       struct table_cell cell;
633       table_get_cell (title, 0, 0, &cell);
634       html_put_table_cell (html, pt, &cell, "caption", NULL);
635     }
636
637   if (layers)
638     {
639       fputs ("<thead>\n", html->file);
640       for (size_t y = 0; y < layers->n[V]; y++)
641         {
642           fputs ("<tr>\n", html->file);
643
644           struct table_cell cell;
645           table_get_cell (layers, 0, y, &cell);
646           cell.d[H][1] = body->n[H];
647           html_put_table_cell (html, pt, &cell, "td", NULL);
648
649           fputs ("</tr>\n", html->file);
650         }
651       fputs ("</thead>\n", html->file);
652     }
653
654   fputs ("<tbody>\n", html->file);
655   for (int y = 0; y < body->n[V]; y++)
656     {
657       fputs ("<tr>\n", html->file);
658       for (int x = 0; x < body->n[H]; )
659         {
660           struct table_cell cell;
661           table_get_cell (body, x, y, &cell);
662           if (x == cell.d[TABLE_HORZ][0] && y == cell.d[TABLE_VERT][0])
663             {
664               bool is_header = (y < body->h[V][0]
665                                 || y >= body->n[V] - body->h[V][1]
666                                 || x < body->h[H][0]
667                                 || x >= body->n[H] - body->h[H][1]);
668               const char *tag = is_header ? "th" : "td";
669               html_put_table_cell (html, pt, &cell, tag, body);
670             }
671
672           x = cell.d[TABLE_HORZ][1];
673         }
674       fputs ("</tr>\n", html->file);
675     }
676   fputs ("</tbody>\n", html->file);
677
678   if (caption || footnotes)
679     {
680       fprintf (html->file, "<tfoot>\n");
681
682       if (caption)
683         {
684           fputs ("<tr>\n", html->file);
685
686           struct table_cell cell;
687           table_get_cell (caption, 0, 0, &cell);
688           cell.d[H][1] = body->n[H];
689           html_put_table_cell (html, pt, &cell, "td", NULL);
690
691           fputs ("</tr>\n", html->file);
692         }
693
694       if (footnotes)
695         for (size_t y = 0; y < footnotes->n[V]; y++)
696           {
697             fputs ("<tr>\n", html->file);
698
699             struct table_cell cell;
700             table_get_cell (footnotes, 0, y, &cell);
701             cell.d[H][1] = body->n[H];
702             html_put_table_cell (html, pt, &cell, "td", NULL);
703
704             fputs ("</tr>\n", html->file);
705           }
706       fputs ("</tfoot>\n", html->file);
707     }
708
709   fputs ("</table>\n\n", html->file);
710
711   table_unref (title);
712   table_unref (layers);
713   table_unref (body);
714   table_unref (caption);
715   table_unref (footnotes);
716 }
717
718 static void
719 html_output_table (struct html_driver *html, const struct table_item *item)
720 {
721   size_t *layer_indexes;
722   PIVOT_OUTPUT_FOR_EACH_LAYER (layer_indexes, item->pt, true)
723     html_output_table_layer (html, item->pt, layer_indexes);
724 }
725
726 struct output_driver_factory html_driver_factory =
727   { "html", "pspp.html", html_create };
728
729 static const struct output_driver_class html_driver_class =
730   {
731     "html",
732     html_destroy,
733     html_submit,
734     NULL,
735   };