1 /* $OpenBSD: tbl_term.c,v 1.48 2018/08/19 23:10:16 schwarze Exp $ */ 2 /* 3 * Copyright (c) 2009, 2011 Kristaps Dzonsons <kristaps@bsd.lv> 4 * Copyright (c) 2011-2018 Ingo Schwarze <schwarze@openbsd.org> 5 * 6 * Permission to use, copy, modify, and distribute this software for any 7 * purpose with or without fee is hereby granted, provided that the above 8 * copyright notice and this permission notice appear in all copies. 9 * 10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 */ 18 #include <sys/types.h> 19 20 #include <assert.h> 21 #include <ctype.h> 22 #include <stdio.h> 23 #include <stdlib.h> 24 #include <string.h> 25 26 #include "mandoc.h" 27 #include "out.h" 28 #include "term.h" 29 30 #define IS_HORIZ(cp) ((cp)->pos == TBL_CELL_HORIZ || \ 31 (cp)->pos == TBL_CELL_DHORIZ) 32 33 static size_t term_tbl_len(size_t, void *); 34 static size_t term_tbl_strlen(const char *, void *); 35 static size_t term_tbl_sulen(const struct roffsu *, void *); 36 static void tbl_char(struct termp *, char, size_t); 37 static void tbl_data(struct termp *, const struct tbl_opts *, 38 const struct tbl_cell *, 39 const struct tbl_dat *, 40 const struct roffcol *); 41 static void tbl_literal(struct termp *, const struct tbl_dat *, 42 const struct roffcol *); 43 static void tbl_number(struct termp *, const struct tbl_opts *, 44 const struct tbl_dat *, 45 const struct roffcol *); 46 static void tbl_hrule(struct termp *, const struct tbl_span *, int); 47 static void tbl_word(struct termp *, const struct tbl_dat *); 48 49 50 static size_t 51 term_tbl_sulen(const struct roffsu *su, void *arg) 52 { 53 int i; 54 55 i = term_hen((const struct termp *)arg, su); 56 return i > 0 ? i : 0; 57 } 58 59 static size_t 60 term_tbl_strlen(const char *p, void *arg) 61 { 62 return term_strlen((const struct termp *)arg, p); 63 } 64 65 static size_t 66 term_tbl_len(size_t sz, void *arg) 67 { 68 return term_len((const struct termp *)arg, sz); 69 } 70 71 void 72 term_tbl(struct termp *tp, const struct tbl_span *sp) 73 { 74 const struct tbl_cell *cp, *cpn, *cpp; 75 const struct tbl_dat *dp; 76 static size_t offset; 77 size_t coloff, tsz; 78 int ic, horiz, spans, vert, more; 79 char fc; 80 81 /* Inhibit printing of spaces: we do padding ourselves. */ 82 83 tp->flags |= TERMP_NOSPACE | TERMP_NONOSPACE; 84 85 /* 86 * The first time we're invoked for a given table block, 87 * calculate the table widths and decimal positions. 88 */ 89 90 if (tp->tbl.cols == NULL) { 91 tp->tbl.len = term_tbl_len; 92 tp->tbl.slen = term_tbl_strlen; 93 tp->tbl.sulen = term_tbl_sulen; 94 tp->tbl.arg = tp; 95 96 tblcalc(&tp->tbl, sp, tp->tcol->offset, tp->tcol->rmargin); 97 98 /* Tables leak .ta settings to subsequent text. */ 99 100 term_tab_set(tp, NULL); 101 coloff = sp->opts->opts & (TBL_OPT_BOX | TBL_OPT_DBOX) || 102 sp->opts->lvert; 103 for (ic = 0; ic < sp->opts->cols; ic++) { 104 coloff += tp->tbl.cols[ic].width; 105 term_tab_iset(coloff); 106 coloff += tp->tbl.cols[ic].spacing; 107 } 108 109 /* Center the table as a whole. */ 110 111 offset = tp->tcol->offset; 112 if (sp->opts->opts & TBL_OPT_CENTRE) { 113 tsz = sp->opts->opts & (TBL_OPT_BOX | TBL_OPT_DBOX) 114 ? 2 : !!sp->opts->lvert + !!sp->opts->rvert; 115 for (ic = 0; ic + 1 < sp->opts->cols; ic++) 116 tsz += tp->tbl.cols[ic].width + 117 tp->tbl.cols[ic].spacing; 118 if (sp->opts->cols) 119 tsz += tp->tbl.cols[sp->opts->cols - 1].width; 120 if (offset + tsz > tp->tcol->rmargin) 121 tsz -= 1; 122 tp->tcol->offset = offset + tp->tcol->rmargin > tsz ? 123 (offset + tp->tcol->rmargin - tsz) / 2 : 0; 124 } 125 126 /* Horizontal frame at the start of boxed tables. */ 127 128 if (sp->opts->opts & TBL_OPT_DBOX) 129 tbl_hrule(tp, sp, 3); 130 if (sp->opts->opts & (TBL_OPT_DBOX | TBL_OPT_BOX)) 131 tbl_hrule(tp, sp, 2); 132 } 133 134 /* Set up the columns. */ 135 136 tp->flags |= TERMP_MULTICOL; 137 horiz = 0; 138 switch (sp->pos) { 139 case TBL_SPAN_HORIZ: 140 case TBL_SPAN_DHORIZ: 141 horiz = 1; 142 term_setcol(tp, 1); 143 break; 144 case TBL_SPAN_DATA: 145 term_setcol(tp, sp->opts->cols + 2); 146 coloff = tp->tcol->offset; 147 148 /* Set up a column for a left vertical frame. */ 149 150 if (sp->opts->opts & (TBL_OPT_BOX | TBL_OPT_DBOX) || 151 sp->opts->lvert) 152 coloff++; 153 tp->tcol->rmargin = coloff; 154 155 /* Set up the data columns. */ 156 157 dp = sp->first; 158 spans = 0; 159 for (ic = 0; ic < sp->opts->cols; ic++) { 160 if (spans == 0) { 161 tp->tcol++; 162 tp->tcol->offset = coloff; 163 } 164 coloff += tp->tbl.cols[ic].width; 165 tp->tcol->rmargin = coloff; 166 if (ic + 1 < sp->opts->cols) 167 coloff += tp->tbl.cols[ic].spacing; 168 if (spans) { 169 spans--; 170 continue; 171 } 172 if (dp == NULL) 173 continue; 174 spans = dp->spans; 175 if (ic || sp->layout->first->pos != TBL_CELL_SPAN) 176 dp = dp->next; 177 } 178 179 /* Set up a column for a right vertical frame. */ 180 181 tp->tcol++; 182 tp->tcol->offset = coloff + 1; 183 tp->tcol->rmargin = tp->maxrmargin; 184 185 /* Spans may have reduced the number of columns. */ 186 187 tp->lasttcol = tp->tcol - tp->tcols; 188 189 /* Fill the buffers for all data columns. */ 190 191 tp->tcol = tp->tcols; 192 cp = cpn = sp->layout->first; 193 dp = sp->first; 194 spans = 0; 195 for (ic = 0; ic < sp->opts->cols; ic++) { 196 if (cpn != NULL) { 197 cp = cpn; 198 cpn = cpn->next; 199 } 200 if (spans) { 201 spans--; 202 continue; 203 } 204 tp->tcol++; 205 tp->col = 0; 206 tbl_data(tp, sp->opts, cp, dp, tp->tbl.cols + ic); 207 if (dp == NULL) 208 continue; 209 spans = dp->spans; 210 if (cp->pos != TBL_CELL_SPAN) 211 dp = dp->next; 212 } 213 break; 214 } 215 216 do { 217 /* Print the vertical frame at the start of each row. */ 218 219 tp->tcol = tp->tcols; 220 fc = '\0'; 221 if (sp->layout->vert || 222 (sp->next != NULL && sp->next->layout->vert && 223 sp->next->pos == TBL_SPAN_DATA) || 224 (sp->prev != NULL && sp->prev->layout->vert && 225 (horiz || (IS_HORIZ(sp->layout->first) && 226 !IS_HORIZ(sp->prev->layout->first)))) || 227 sp->opts->opts & (TBL_OPT_BOX | TBL_OPT_DBOX)) 228 fc = horiz || IS_HORIZ(sp->layout->first) ? '+' : '|'; 229 else if (horiz && sp->opts->lvert) 230 fc = '-'; 231 if (fc != '\0') { 232 (*tp->advance)(tp, tp->tcols->offset); 233 (*tp->letter)(tp, fc); 234 tp->viscol = tp->tcol->offset + 1; 235 } 236 237 /* Print the data cells. */ 238 239 more = 0; 240 if (horiz) { 241 tbl_hrule(tp, sp, 0); 242 term_flushln(tp); 243 } else { 244 cp = sp->layout->first; 245 cpn = sp->next == NULL ? NULL : 246 sp->next->layout->first; 247 cpp = sp->prev == NULL ? NULL : 248 sp->prev->layout->first; 249 dp = sp->first; 250 spans = 0; 251 for (ic = 0; ic < sp->opts->cols; ic++) { 252 253 /* 254 * Figure out whether to print a 255 * vertical line after this cell 256 * and advance to next layout cell. 257 */ 258 259 if (cp != NULL) { 260 vert = cp->vert; 261 switch (cp->pos) { 262 case TBL_CELL_HORIZ: 263 fc = '-'; 264 break; 265 case TBL_CELL_DHORIZ: 266 fc = '='; 267 break; 268 default: 269 fc = ' '; 270 break; 271 } 272 } else { 273 vert = 0; 274 fc = ' '; 275 } 276 if (cpp != NULL) { 277 if (vert == 0 && 278 cp != NULL && 279 ((IS_HORIZ(cp) && 280 !IS_HORIZ(cpp)) || 281 (cp->next != NULL && 282 cpp->next != NULL && 283 IS_HORIZ(cp->next) && 284 !IS_HORIZ(cpp->next)))) 285 vert = cpp->vert; 286 cpp = cpp->next; 287 } 288 if (vert == 0 && 289 sp->opts->opts & TBL_OPT_ALLBOX) 290 vert = 1; 291 if (cpn != NULL) { 292 if (vert == 0) 293 vert = cpn->vert; 294 cpn = cpn->next; 295 } 296 if (cp != NULL) 297 cp = cp->next; 298 299 /* 300 * Skip later cells in a span, 301 * figure out whether to start a span, 302 * and advance to next data cell. 303 */ 304 305 if (spans) { 306 spans--; 307 continue; 308 } 309 if (dp != NULL) { 310 spans = dp->spans; 311 if (ic || sp->layout->first->pos 312 != TBL_CELL_SPAN) 313 dp = dp->next; 314 } 315 316 /* 317 * Print one line of text in the cell 318 * and remember whether there is more. 319 */ 320 321 tp->tcol++; 322 if (tp->tcol->col < tp->tcol->lastcol) 323 term_flushln(tp); 324 if (tp->tcol->col < tp->tcol->lastcol) 325 more = 1; 326 327 /* 328 * Vertical frames between data cells, 329 * but not after the last column. 330 */ 331 332 if (fc == ' ' && ((vert == 0 && 333 (cp == NULL || !IS_HORIZ(cp))) || 334 tp->tcol + 1 == tp->tcols + tp->lasttcol)) 335 continue; 336 337 if (tp->viscol < tp->tcol->rmargin) { 338 (*tp->advance)(tp, tp->tcol->rmargin 339 - tp->viscol); 340 tp->viscol = tp->tcol->rmargin; 341 } 342 while (tp->viscol < tp->tcol->rmargin + 343 tp->tbl.cols[ic].spacing / 2) { 344 (*tp->letter)(tp, fc); 345 tp->viscol++; 346 } 347 348 if (tp->tcol + 1 == tp->tcols + tp->lasttcol) 349 continue; 350 351 if (fc == ' ' && cp != NULL) { 352 switch (cp->pos) { 353 case TBL_CELL_HORIZ: 354 fc = '-'; 355 break; 356 case TBL_CELL_DHORIZ: 357 fc = '='; 358 break; 359 default: 360 break; 361 } 362 } 363 if (tp->tbl.cols[ic].spacing) { 364 (*tp->letter)(tp, fc == ' ' ? '|' : 365 vert ? '+' : fc); 366 tp->viscol++; 367 } 368 369 if (fc != ' ') { 370 if (cp != NULL && 371 cp->pos == TBL_CELL_HORIZ) 372 fc = '-'; 373 else if (cp != NULL && 374 cp->pos == TBL_CELL_DHORIZ) 375 fc = '='; 376 else 377 fc = ' '; 378 } 379 if (tp->tbl.cols[ic].spacing > 2 && 380 (vert > 1 || fc != ' ')) { 381 (*tp->letter)(tp, fc == ' ' ? '|' : 382 vert > 1 ? '+' : fc); 383 tp->viscol++; 384 } 385 } 386 } 387 388 /* Print the vertical frame at the end of each row. */ 389 390 fc = '\0'; 391 if ((sp->layout->last->vert && 392 sp->layout->last->col + 1 == sp->opts->cols) || 393 (sp->next != NULL && 394 sp->next->layout->last->vert && 395 sp->next->layout->last->col + 1 == sp->opts->cols) || 396 (sp->prev != NULL && 397 sp->prev->layout->last->vert && 398 sp->prev->layout->last->col + 1 == sp->opts->cols && 399 (horiz || (IS_HORIZ(sp->layout->last) && 400 !IS_HORIZ(sp->prev->layout->last)))) || 401 (sp->opts->opts & (TBL_OPT_BOX | TBL_OPT_DBOX))) 402 fc = horiz || IS_HORIZ(sp->layout->last) ? '+' : '|'; 403 else if (horiz && sp->opts->rvert) 404 fc = '-'; 405 if (fc != '\0') { 406 if (horiz == 0 && (IS_HORIZ(sp->layout->last) == 0 || 407 sp->layout->last->col + 1 < sp->opts->cols)) { 408 tp->tcol++; 409 (*tp->advance)(tp, 410 tp->tcol->offset > tp->viscol ? 411 tp->tcol->offset - tp->viscol : 1); 412 } 413 (*tp->letter)(tp, fc); 414 } 415 (*tp->endline)(tp); 416 tp->viscol = 0; 417 } while (more); 418 419 /* 420 * Clean up after this row. If it is the last line 421 * of the table, print the box line and clean up 422 * column data; otherwise, print the allbox line. 423 */ 424 425 term_setcol(tp, 1); 426 tp->flags &= ~TERMP_MULTICOL; 427 tp->tcol->rmargin = tp->maxrmargin; 428 if (sp->next == NULL) { 429 if (sp->opts->opts & (TBL_OPT_DBOX | TBL_OPT_BOX)) { 430 tbl_hrule(tp, sp, 2); 431 tp->skipvsp = 1; 432 } 433 if (sp->opts->opts & TBL_OPT_DBOX) { 434 tbl_hrule(tp, sp, 3); 435 tp->skipvsp = 2; 436 } 437 assert(tp->tbl.cols); 438 free(tp->tbl.cols); 439 tp->tbl.cols = NULL; 440 tp->tcol->offset = offset; 441 } else if (horiz == 0 && sp->opts->opts & TBL_OPT_ALLBOX && 442 (sp->next == NULL || sp->next->pos == TBL_SPAN_DATA || 443 sp->next->next != NULL)) 444 tbl_hrule(tp, sp, 1); 445 446 tp->flags &= ~TERMP_NONOSPACE; 447 } 448 449 /* 450 * Kinds of horizontal rulers: 451 * 0: inside the table (single or double line with crossings) 452 * 1: inside the table (single or double line with crossings and ends) 453 * 2: inner frame (single line with crossings and ends) 454 * 3: outer frame (single line without crossings with ends) 455 */ 456 static void 457 tbl_hrule(struct termp *tp, const struct tbl_span *sp, int kind) 458 { 459 const struct tbl_cell *cp, *cpn, *cpp; 460 const struct roffcol *col; 461 int vert; 462 char cross, line, stdcross, stdline; 463 464 stdline = (kind < 2 && TBL_SPAN_DHORIZ == sp->pos) ? '=' : '-'; 465 stdcross = (kind < 3) ? '+' : '-'; 466 467 cp = sp->layout->first; 468 cpp = kind || sp->prev == NULL ? NULL : sp->prev->layout->first; 469 if (cpp == cp) 470 cpp = NULL; 471 cpn = kind > 1 || sp->next == NULL ? NULL : sp->next->layout->first; 472 if (cpn == cp) 473 cpn = NULL; 474 if (kind) 475 term_word(tp, 476 cpn == NULL || cpn->pos != TBL_CELL_DOWN ? "+" : "|"); 477 for (;;) { 478 col = tp->tbl.cols + cp->col; 479 if (cpn == NULL || cpn->pos != TBL_CELL_DOWN) { 480 line = stdline; 481 cross = stdcross; 482 } else { 483 line = ' '; 484 cross = (kind < 3) ? '|' : ' '; 485 } 486 tbl_char(tp, line, col->width + col->spacing / 2); 487 vert = cp->vert; 488 if ((cp = cp->next) == NULL) 489 break; 490 if (cpp != NULL) { 491 if (vert < cpp->vert) 492 vert = cpp->vert; 493 cpp = cpp->next; 494 } 495 if (cpn != NULL) { 496 if (vert < cpn->vert) 497 vert = cpn->vert; 498 cpn = cpn->next; 499 } 500 if (cpn == NULL || cpn->pos != TBL_CELL_DOWN) { 501 line = stdline; 502 cross = stdcross; 503 } else 504 line = ' '; 505 if (sp->opts->opts & TBL_OPT_ALLBOX && !vert) 506 vert = 1; 507 if (col->spacing) 508 tbl_char(tp, vert ? cross : line, 1); 509 if (col->spacing > 2) 510 tbl_char(tp, vert > 1 ? cross : line, 1); 511 if (col->spacing > 4) 512 tbl_char(tp, line, (col->spacing - 3) / 2); 513 } 514 if (kind) { 515 term_word(tp, 516 cpn == NULL || cpn->pos != TBL_CELL_DOWN ? "+" : "|"); 517 term_flushln(tp); 518 } 519 } 520 521 static void 522 tbl_data(struct termp *tp, const struct tbl_opts *opts, 523 const struct tbl_cell *cp, const struct tbl_dat *dp, 524 const struct roffcol *col) 525 { 526 switch (cp->pos) { 527 case TBL_CELL_HORIZ: 528 tbl_char(tp, '-', col->width); 529 return; 530 case TBL_CELL_DHORIZ: 531 tbl_char(tp, '=', col->width); 532 return; 533 default: 534 break; 535 } 536 537 if (dp == NULL) 538 return; 539 540 switch (dp->pos) { 541 case TBL_DATA_NONE: 542 return; 543 case TBL_DATA_HORIZ: 544 case TBL_DATA_NHORIZ: 545 tbl_char(tp, '-', col->width); 546 return; 547 case TBL_DATA_NDHORIZ: 548 case TBL_DATA_DHORIZ: 549 tbl_char(tp, '=', col->width); 550 return; 551 default: 552 break; 553 } 554 555 switch (cp->pos) { 556 case TBL_CELL_LONG: 557 case TBL_CELL_CENTRE: 558 case TBL_CELL_LEFT: 559 case TBL_CELL_RIGHT: 560 tbl_literal(tp, dp, col); 561 break; 562 case TBL_CELL_NUMBER: 563 tbl_number(tp, opts, dp, col); 564 break; 565 case TBL_CELL_DOWN: 566 case TBL_CELL_SPAN: 567 break; 568 default: 569 abort(); 570 } 571 } 572 573 static void 574 tbl_char(struct termp *tp, char c, size_t len) 575 { 576 size_t i, sz; 577 char cp[2]; 578 579 cp[0] = c; 580 cp[1] = '\0'; 581 582 sz = term_strlen(tp, cp); 583 584 for (i = 0; i < len; i += sz) 585 term_word(tp, cp); 586 } 587 588 static void 589 tbl_literal(struct termp *tp, const struct tbl_dat *dp, 590 const struct roffcol *col) 591 { 592 size_t len, padl, padr, width; 593 int ic, spans; 594 595 assert(dp->string); 596 len = term_strlen(tp, dp->string); 597 width = col->width; 598 ic = dp->layout->col; 599 spans = dp->spans; 600 while (spans--) 601 width += tp->tbl.cols[++ic].width + 3; 602 603 padr = width > len ? width - len : 0; 604 padl = 0; 605 606 switch (dp->layout->pos) { 607 case TBL_CELL_LONG: 608 padl = term_len(tp, 1); 609 padr = padr > padl ? padr - padl : 0; 610 break; 611 case TBL_CELL_CENTRE: 612 if (2 > padr) 613 break; 614 padl = padr / 2; 615 padr -= padl; 616 break; 617 case TBL_CELL_RIGHT: 618 padl = padr; 619 padr = 0; 620 break; 621 default: 622 break; 623 } 624 625 tbl_char(tp, ASCII_NBRSP, padl); 626 tbl_word(tp, dp); 627 tbl_char(tp, ASCII_NBRSP, padr); 628 } 629 630 static void 631 tbl_number(struct termp *tp, const struct tbl_opts *opts, 632 const struct tbl_dat *dp, 633 const struct roffcol *col) 634 { 635 const char *cp, *lastdigit, *lastpoint; 636 size_t intsz, padl, totsz; 637 char buf[2]; 638 639 /* 640 * Almost the same code as in tblcalc_number(): 641 * First find the position of the decimal point. 642 */ 643 644 assert(dp->string); 645 lastdigit = lastpoint = NULL; 646 for (cp = dp->string; cp[0] != '\0'; cp++) { 647 if (cp[0] == '\\' && cp[1] == '&') { 648 lastdigit = lastpoint = cp; 649 break; 650 } else if (cp[0] == opts->decimal && 651 (isdigit((unsigned char)cp[1]) || 652 (cp > dp->string && isdigit((unsigned char)cp[-1])))) 653 lastpoint = cp; 654 else if (isdigit((unsigned char)cp[0])) 655 lastdigit = cp; 656 } 657 658 /* Then measure both widths. */ 659 660 padl = 0; 661 totsz = term_strlen(tp, dp->string); 662 if (lastdigit != NULL) { 663 if (lastpoint == NULL) 664 lastpoint = lastdigit + 1; 665 intsz = 0; 666 buf[1] = '\0'; 667 for (cp = dp->string; cp < lastpoint; cp++) { 668 buf[0] = cp[0]; 669 intsz += term_strlen(tp, buf); 670 } 671 672 /* 673 * Pad left to match the decimal position, 674 * but avoid exceeding the total column width. 675 */ 676 677 if (col->decimal > intsz && col->width > totsz) { 678 padl = col->decimal - intsz; 679 if (padl + totsz > col->width) 680 padl = col->width - totsz; 681 } 682 683 /* If it is not a number, simply center the string. */ 684 685 } else if (col->width > totsz) 686 padl = (col->width - totsz) / 2; 687 688 tbl_char(tp, ASCII_NBRSP, padl); 689 tbl_word(tp, dp); 690 691 /* Pad right to fill the column. */ 692 693 if (col->width > padl + totsz) 694 tbl_char(tp, ASCII_NBRSP, col->width - padl - totsz); 695 } 696 697 static void 698 tbl_word(struct termp *tp, const struct tbl_dat *dp) 699 { 700 int prev_font; 701 702 prev_font = tp->fonti; 703 if (dp->layout->flags & TBL_CELL_BOLD) 704 term_fontpush(tp, TERMFONT_BOLD); 705 else if (dp->layout->flags & TBL_CELL_ITALIC) 706 term_fontpush(tp, TERMFONT_UNDER); 707 708 term_word(tp, dp->string); 709 710 term_fontpopq(tp, prev_font); 711 } 712