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