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