Change license from GPLv2+ to GPLv3+.
[pspp-builds.git] / src / output / ascii.c
1 /* PSPP - a program for statistical analysis.
2    Copyright (C) 1997-9, 2000 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 <ctype.h>
20 #include <errno.h>
21 #include <limits.h>
22 #include <stdlib.h>
23
24 #include <data/file-name.h>
25 #include <libpspp/alloc.h>
26 #include <libpspp/assertion.h>
27 #include <libpspp/compiler.h>
28 #include <libpspp/pool.h>
29 #include <libpspp/start-date.h>
30 #include <libpspp/version.h>
31
32 #include "chart.h"
33 #include "error.h"
34 #include "minmax.h"
35 #include "output.h"
36
37 #include "gettext.h"
38 #define _(msgid) gettext (msgid)
39
40 /* ASCII driver options: (defaults listed first)
41
42    output-file="pspp.list"
43    paginate=on|off              Formfeeds are desired?
44    tab-width=8                  Width of a tab; 0 to not use tabs.
45
46    headers=on|off               Put headers at top of page?
47    emphasis=bold|underline|none Style to use for emphasis.
48    length=66
49    width=130
50    squeeze=off|on               Squeeze multiple newlines into exactly one.
51
52    top-margin=2
53    bottom-margin=2
54
55    box[x]="strng"               Sets box character X (X in base 4: 0-3333).
56  */
57
58 /* Disable messages by failed range checks. */
59 /*#define SUPPRESS_WARNINGS 1 */
60
61 /* Line styles bit shifts. */
62 enum
63   {
64     LNS_TOP = 0,
65     LNS_LEFT = 2,
66     LNS_BOTTOM = 4,
67     LNS_RIGHT = 6,
68
69     LNS_COUNT = 256
70   };
71
72 /* Character attributes. */
73 #define ATTR_EMPHASIS   0x100   /* Bold-face. */
74 #define ATTR_BOX        0x200   /* Line drawing character. */
75
76 /* A line of text. */
77 struct line
78   {
79     unsigned short *chars;      /* Characters and attributes. */
80     int char_cnt;               /* Length. */
81     int char_cap;               /* Allocated bytes. */
82   };
83
84 /* How to emphasize text. */
85 enum emphasis_style
86   {
87     EMPH_BOLD,                  /* Overstrike for bold. */
88     EMPH_UNDERLINE,             /* Overstrike for underlining. */
89     EMPH_NONE                   /* No emphasis. */
90   };
91
92 /* ASCII output driver extension record. */
93 struct ascii_driver_ext
94   {
95     struct pool *pool;
96
97     /* User parameters. */
98     bool headers;               /* Print headers at top of page? */
99     bool paginate;              /* Insert formfeeds? */
100     bool squeeze_blank_lines;   /* Squeeze multiple blank lines into one? */
101     enum emphasis_style emphasis; /* How to emphasize text. */
102     int tab_width;              /* Width of a tab; 0 not to use tabs. */
103
104     int page_length;            /* Page length before subtracting margins. */
105     int top_margin;             /* Top margin in lines. */
106     int bottom_margin;          /* Bottom margin in lines. */
107
108     char *box[LNS_COUNT];       /* Line & box drawing characters. */
109
110     /* Internal state. */
111     char *file_name;            /* Output file name. */
112     FILE *file;                 /* Output file. */
113     int page_number;            /* Current page number. */
114     struct line *lines;         /* Page content. */
115     int line_cap;               /* Number of lines allocated. */
116   };
117
118 static int get_default_box_char (size_t idx);
119 static bool handle_option (struct outp_driver *this, const char *key,
120                            const struct string *val);
121
122 static bool
123 ascii_open_driver (struct outp_driver *this, struct substring options)
124 {
125   struct ascii_driver_ext *x;
126   int i;
127
128   this->width = 79;
129   this->font_height = 1;
130   this->prop_em_width = 1;
131   this->fixed_width = 1;
132   for (i = 0; i < OUTP_L_COUNT; i++)
133     this->horiz_line_width[i] = this->vert_line_width[i] = i != OUTP_L_NONE;
134
135   this->ext = x = pool_create_container (struct ascii_driver_ext, pool);
136   x->headers = true;
137   x->paginate = true;
138   x->squeeze_blank_lines = false;
139   x->emphasis = EMPH_BOLD;
140   x->tab_width = 8;
141   x->page_length = 66;
142   x->top_margin = 2;
143   x->bottom_margin = 2;
144   for (i = 0; i < LNS_COUNT; i++)
145     x->box[i] = NULL;
146   x->file_name = pool_strdup (x->pool, "pspp.list");
147   x->file = NULL;
148   x->page_number = 0;
149   x->lines = NULL;
150   x->line_cap = 0;
151
152   if (!outp_parse_options (options, handle_option, this))
153     goto error;
154
155   x->file = pool_fopen (x->pool, x->file_name, "w");
156   if (x->file == NULL)
157     {
158       error (0, errno, _("ascii: opening output file \"%s\""), x->file_name);
159       goto error;
160     }
161
162   this->length = x->page_length - x->top_margin - x->bottom_margin - 1;
163   if (x->headers)
164     this->length -= 3;
165
166   if (this->width < 59 || this->length < 15)
167     {
168       error (0, 0,
169              _("ascii: page excluding margins and headers "
170                "must be at least 59 characters wide by 15 lines long, but as "
171                "configured is only %d characters by %d lines"),
172              this->width, this->length);
173       return false;
174     }
175
176   for (i = 0; i < LNS_COUNT; i++)
177     if (x->box[i] == NULL)
178       {
179         char s[2];
180         s[0] = get_default_box_char (i);
181         s[1] = '\0';
182         x->box[i] = pool_strdup (x->pool, s);
183       }
184
185   return true;
186
187  error:
188   pool_destroy (x->pool);
189   return false;
190 }
191
192 static int
193 get_default_box_char (size_t idx)
194 {
195   /* Disassemble IDX into components. */
196   unsigned top = (idx >> LNS_TOP) & 3;
197   unsigned left = (idx >> LNS_LEFT) & 3;
198   unsigned bottom = (idx >> LNS_BOTTOM) & 3;
199   unsigned right = (idx >> LNS_RIGHT) & 3;
200
201   /* Reassemble components into nibbles in the order TLBR.
202      This makes it easy to read the case labels. */
203   unsigned value = (top << 12) | (left << 8) | (bottom << 4) | (right << 0);
204   switch (value)
205     {
206     case 0x0000:
207       return ' ';
208
209     case 0x0100: case 0x0101: case 0x0001:
210       return '-';
211
212     case 0x1000: case 0x1010: case 0x0010:
213       return '|';
214
215     case 0x0300: case 0x0303: case 0x0003:
216     case 0x0200: case 0x0202: case 0x0002:
217       return '=';
218
219     default:
220       return left > 1 || top > 1 || right > 1 || bottom > 1 ? '#' : '+';
221     }
222 }
223
224 static bool
225 ascii_close_driver (struct outp_driver *this)
226 {
227   struct ascii_driver_ext *x = this->ext;
228
229   if (fn_close (x->file_name, x->file) != 0)
230     error (0, errno, _("ascii: closing output file \"%s\""), x->file_name);
231   pool_detach_file (x->pool, x->file);
232   pool_destroy (x->pool);
233
234   return true;
235 }
236
237 /* Generic option types. */
238 enum
239   {
240     boolean_arg,
241     string_arg,
242     nonneg_int_arg,
243     pos_int_arg,
244     output_file_arg
245   };
246
247 static const struct outp_option option_tab[] =
248   {
249     {"headers", boolean_arg, 0},
250     {"paginate", boolean_arg, 1},
251     {"squeeze", boolean_arg, 2},
252
253     {"emphasis", string_arg, 3},
254
255     {"output-file", output_file_arg, 0},
256
257     {"length", pos_int_arg, 0},
258     {"width", pos_int_arg, 1},
259
260     {"top-margin", nonneg_int_arg, 0},
261     {"bottom-margin", nonneg_int_arg, 1},
262     {"tab-width", nonneg_int_arg, 2},
263
264     {NULL, 0, 0},
265   };
266
267 static bool
268 handle_option (struct outp_driver *this, const char *key,
269                const struct string *val)
270 {
271   struct ascii_driver_ext *x = this->ext;
272   int subcat;
273   const char *value;
274
275   value = ds_cstr (val);
276   if (!strncmp (key, "box[", 4))
277     {
278       char *tail;
279       int indx = strtol (&key[4], &tail, 4);
280       if (*tail != ']' || indx < 0 || indx > LNS_COUNT)
281         {
282           error (0, 0, _("ascii: bad index value for `box' key: syntax "
283                          "is box[INDEX], 0 <= INDEX < %d decimal, with INDEX "
284                          "expressed in base 4"),
285                  LNS_COUNT);
286           return false;
287         }
288       if (x->box[indx] != NULL)
289         error (0, 0, _("ascii: multiple values for %s"), key);
290       x->box[indx] = pool_strdup (x->pool, value);
291       return true;
292     }
293
294   switch (outp_match_keyword (key, option_tab, &subcat))
295     {
296     case -1:
297       error (0, 0, _("ascii: unknown parameter `%s'"), key);
298       break;
299     case output_file_arg:
300       x->file_name = pool_strdup (x->pool, value);
301       break;
302     case pos_int_arg:
303       {
304         char *tail;
305         int arg;
306
307         errno = 0;
308         arg = strtol (value, &tail, 0);
309         if (arg < 1 || errno == ERANGE || *tail)
310           {
311             error (0, 0, _("ascii: positive integer required as `%s' value"),
312                    key);
313             break;
314           }
315         switch (subcat)
316           {
317           case 0:
318             x->page_length = arg;
319             break;
320           case 1:
321             this->width = arg;
322             break;
323           default:
324             NOT_REACHED ();
325           }
326       }
327       break;
328     case string_arg:
329       if (!strcmp (value, "bold"))
330         x->emphasis = EMPH_BOLD;
331       else if (!strcmp (value, "underline"))
332         x->emphasis = EMPH_UNDERLINE;
333       else if (!strcmp (value, "none"))
334         x->emphasis = EMPH_NONE;
335       else
336         error (0, 0,
337                _("ascii: `emphasis' value must be `bold', "
338                  "`underline', or `none'"));
339       break;
340     case nonneg_int_arg:
341       {
342         char *tail;
343         int arg;
344
345         errno = 0;
346         arg = strtol (value, &tail, 0);
347         if (arg < 0 || errno == ERANGE || *tail)
348           {
349             error (0, 0,
350                    _("ascii: zero or positive integer required as `%s' value"),
351                    key);
352             break;
353           }
354         switch (subcat)
355           {
356           case 0:
357             x->top_margin = arg;
358             break;
359           case 1:
360             x->bottom_margin = arg;
361             break;
362           case 2:
363             x->tab_width = arg;
364             break;
365           default:
366             NOT_REACHED ();
367           }
368       }
369       break;
370     case boolean_arg:
371       {
372         bool setting;
373         if (!strcmp (value, "on") || !strcmp (value, "true")
374             || !strcmp (value, "yes") || atoi (value))
375           setting = true;
376         else if (!strcmp (value, "off") || !strcmp (value, "false")
377                  || !strcmp (value, "no") || !strcmp (value, "0"))
378           setting = false;
379         else
380           {
381             error (0, 0, _("ascii: boolean value expected for `%s'"), key);
382             return false;
383           }
384         switch (subcat)
385           {
386           case 0:
387             x->headers = setting;
388             break;
389           case 1:
390             x->paginate = setting;
391             break;
392           case 2:
393             x->squeeze_blank_lines = setting;
394             break;
395           default:
396             NOT_REACHED ();
397           }
398       }
399       break;
400     default:
401       NOT_REACHED ();
402     }
403
404   return true;
405 }
406
407 static void
408 ascii_open_page (struct outp_driver *this)
409 {
410   struct ascii_driver_ext *x = this->ext;
411   int i;
412
413   x->page_number++;
414
415   if (this->length > x->line_cap)
416     {
417       x->lines = pool_nrealloc (x->pool,
418                                 x->lines, this->length, sizeof *x->lines);
419       for (i = x->line_cap; i < this->length; i++)
420         {
421           struct line *line = &x->lines[i];
422           line->chars = NULL;
423           line->char_cap = 0;
424         }
425       x->line_cap = this->length;
426     }
427
428   for (i = 0; i < this->length; i++)
429     x->lines[i].char_cnt = 0;
430 }
431
432 /* Ensures that at least the first LENGTH characters of line Y in
433    THIS driver identified X have been cleared out. */
434 static inline void
435 expand_line (struct outp_driver *this, int y, int length)
436 {
437   struct ascii_driver_ext *ext = this->ext;
438   struct line *line = &ext->lines[y];
439   if (line->char_cnt < length)
440     {
441       int x;
442       if (line->char_cap < length)
443         {
444           line->char_cap = MIN (length * 2, this->width);
445           line->chars = pool_nrealloc (ext->pool,
446                                        line->chars,
447                                        line->char_cap, sizeof *line->chars);
448         }
449       for (x = line->char_cnt; x < length; x++)
450         line->chars[x] = ' ';
451       line->char_cnt = length;
452     }
453 }
454
455 static void
456 ascii_line (struct outp_driver *this,
457             int x0, int y0, int x1, int y1,
458             enum outp_line_style top, enum outp_line_style left,
459             enum outp_line_style bottom, enum outp_line_style right)
460 {
461   struct ascii_driver_ext *ext = this->ext;
462   int y;
463   unsigned short value;
464
465   assert (this->page_open);
466 #if DEBUGGING
467   if (x0 < 0 || x1 > this->width || y0 < 0 || y1 > this->length)
468     {
469 #if !SUPPRESS_WARNINGS
470       printf (_("ascii: bad line (%d,%d)-(%d,%d) out of (%d,%d)\n"),
471               x0, y0, x1, y1, this->width, this->length);
472 #endif
473       return;
474     }
475 #endif
476
477   value = ((left << LNS_LEFT) | (right << LNS_RIGHT)
478            | (top << LNS_TOP) | (bottom << LNS_BOTTOM) | ATTR_BOX);
479   for (y = y0; y < y1; y++)
480     {
481       int x;
482
483       expand_line (this, y, x1);
484       for (x = x0; x < x1; x++)
485         ext->lines[y].chars[x] = value;
486     }
487 }
488
489 static void
490 text_draw (struct outp_driver *this,
491            enum outp_font font,
492            int x, int y,
493            enum outp_justification justification, int width,
494            const char *string, size_t length)
495 {
496   struct ascii_driver_ext *ext = this->ext;
497   unsigned short attr = font == OUTP_EMPHASIS ? ATTR_EMPHASIS : 0;
498
499   int line_len;
500
501   switch (justification)
502     {
503     case OUTP_LEFT:
504       break;
505     case OUTP_CENTER:
506       x += (width - length + 1) / 2;
507       break;
508     case OUTP_RIGHT:
509       x += width - length;
510       break;
511     default:
512       NOT_REACHED ();
513     }
514
515   if (y >= this->length || x >= this->width)
516     return;
517
518   if (x + length > this->width)
519     length = this->width - x;
520
521   line_len = x + length;
522
523   expand_line (this, y, line_len);
524   while (length-- > 0)
525     ext->lines[y].chars[x++] = *string++ | attr;
526 }
527
528 /* Divides the text T->S into lines of width T->H.  Sets T->V to the
529    number of lines necessary.  Actually draws the text if DRAW is
530    true. */
531 static void
532 delineate (struct outp_driver *this, const struct outp_text *text, bool draw,
533            int *width, int *height)
534 {
535   int max_width;
536   int height_left;
537
538   const char *cp = ss_data (text->string);
539
540   max_width = 0;
541   height_left = text->v;
542
543   while (height_left > 0)
544     {
545       size_t chars_left;
546       size_t line_len;
547       const char *end;
548
549       /* Initially the line is up to text->h characters long. */
550       chars_left = ss_end (text->string) - cp;
551       if (chars_left == 0)
552         break;
553       line_len = MIN (chars_left, text->h);
554
555       /* A new-line terminates the line prematurely. */
556       end = memchr (cp, '\n', line_len);
557       if (end != NULL)
558         line_len = end - cp;
559
560       /* Don't cut off words if it can be avoided. */
561       if (cp + line_len < ss_end (text->string))
562         {
563           size_t space_len = line_len;
564           while (space_len > 0 && !isspace ((unsigned char) cp[space_len]))
565             space_len--;
566           if (space_len > 0)
567             line_len = space_len;
568         }
569
570       /* Draw text. */
571       if (draw)
572         text_draw (this,
573                    text->font,
574                    text->x, text->y + (text->v - height_left),
575                    text->justification, text->h,
576                    cp, line_len);
577
578       /* Update. */
579       height_left--;
580       if (line_len > max_width)
581         max_width = line_len;
582
583       /* Next line. */
584       cp += line_len;
585       if (cp < ss_end (text->string) && isspace ((unsigned char) *cp))
586         cp++;
587     }
588
589   if (width != NULL)
590     *width = max_width;
591   if (height != NULL)
592     *height = text->v - height_left;
593 }
594
595 static void
596 ascii_text_metrics (struct outp_driver *this, const struct outp_text *t,
597                     int *width, int *height)
598 {
599   delineate (this, t, false, width, height);
600 }
601
602 static void
603 ascii_text_draw (struct outp_driver *this, const struct outp_text *t)
604 {
605   assert (this->page_open);
606   delineate (this, t, true, NULL, NULL);
607 }
608
609 \f
610 /* ascii_close_page () and support routines. */
611
612 /* Writes the LENGTH characters in S to OUT.  */
613 static void
614 output_line (struct outp_driver *this, const struct line *line,
615              struct string *out)
616 {
617   struct ascii_driver_ext *ext = this->ext;
618   const unsigned short *s = line->chars;
619   size_t length;
620
621   for (length = line->char_cnt; length-- > 0; s++)
622     if (*s & ATTR_BOX)
623       ds_put_cstr (out, ext->box[*s & 0xff]);
624     else
625       {
626         if (*s & ATTR_EMPHASIS)
627           {
628             if (ext->emphasis == EMPH_BOLD)
629               {
630                 ds_put_char (out, *s);
631                 ds_put_char (out, '\b');
632               }
633             else if (ext->emphasis == EMPH_UNDERLINE)
634               ds_put_cstr (out, "_\b");
635           }
636         ds_put_char (out, *s);
637       }
638 }
639
640 static void
641 append_lr_justified (struct string *out, int width,
642                      const char *left, const char *right)
643 {
644   ds_put_char_multiple (out, ' ', width);
645   if (left != NULL)
646     {
647       size_t length = MIN (strlen (left), width);
648       memcpy (ds_end (out) - width, left, length);
649     }
650   if (right != NULL)
651     {
652       size_t length = MIN (strlen (right), width);
653       memcpy (ds_end (out) - length, right, length);
654     }
655   ds_put_char (out, '\n');
656 }
657
658 static void
659 dump_output (struct outp_driver *this, struct string *out)
660 {
661   struct ascii_driver_ext *x = this->ext;
662   fwrite (ds_data (out), ds_length (out), 1, x->file);
663   ds_clear (out);
664 }
665
666 static void
667 ascii_close_page (struct outp_driver *this)
668 {
669   struct ascii_driver_ext *x = this->ext;
670   struct string out;
671   int line_num;
672
673   ds_init_empty (&out);
674
675   ds_put_char_multiple (&out, '\n', x->top_margin);
676   if (x->headers)
677     {
678       char *r1, *r2;
679
680       r1 = xasprintf (_("%s - Page %d"), get_start_date (), x->page_number);
681       r2 = xasprintf ("%s - %s" , version, host_system);
682
683       append_lr_justified (&out, this->width, outp_title, r1);
684       append_lr_justified (&out, this->width, outp_subtitle, r2);
685       ds_put_char (&out, '\n');
686
687       free (r1);
688       free (r2);
689     }
690   dump_output (this, &out);
691
692   for (line_num = 0; line_num < this->length; line_num++)
693     {
694
695       /* Squeeze multiple blank lines into a single blank line if
696          requested. */
697       if (x->squeeze_blank_lines)
698         {
699           if (line_num >= x->line_cap)
700             break;
701           if (line_num > 0
702               && x->lines[line_num].char_cnt == 0
703               && x->lines[line_num - 1].char_cnt == 0)
704             continue;
705         }
706
707       if (line_num < x->line_cap)
708         output_line (this, &x->lines[line_num], &out);
709       ds_put_char (&out, '\n');
710       dump_output (this, &out);
711     }
712
713   ds_put_char_multiple (&out, '\n', x->bottom_margin);
714   if (x->paginate)
715     ds_put_char (&out, '\f');
716
717   dump_output (this, &out);
718   ds_destroy (&out);
719 }
720
721 static void
722 ascii_chart_initialise (struct outp_driver *d UNUSED, struct chart *ch)
723 {
724   error (0, 0, _("ascii: charts are unsupported by this driver"));
725   ch->lp = 0;
726 }
727
728 static void
729 ascii_chart_finalise (struct outp_driver *d UNUSED, struct chart *ch UNUSED)
730 {
731
732 }
733
734 const struct outp_class ascii_class =
735 {
736   "ascii",
737   0,
738
739   ascii_open_driver,
740   ascii_close_driver,
741
742   ascii_open_page,
743   ascii_close_page,
744
745   NULL,
746
747   ascii_line,
748   ascii_text_metrics,
749   ascii_text_draw,
750
751   ascii_chart_initialise,
752   ascii_chart_finalise
753 };