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