Return to mdocterm.c CVS log | Up to [cvsweb.bsd.lv] / mandoc |
version 1.7, 2009/02/23 15:34:53 | version 1.25, 2009/03/02 17:14:46 | ||
---|---|---|---|
|
|
||
/* $Id$ */ | /* $Id$ */ | ||
/* | /* | ||
* Copyright (c) 2008 Kristaps Dzonsons <kristaps@kth.se> | * Copyright (c) 2008 Kristaps Dzonsons <kristaps@kth.se> | ||
* | * | ||
|
|
||
* TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR | * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR | ||
* PERFORMANCE OF THIS SOFTWARE. | * PERFORMANCE OF THIS SOFTWARE. | ||
*/ | */ | ||
#include <sys/utsname.h> | |||
#include <assert.h> | #include <assert.h> | ||
#include <ctype.h> | #include <ctype.h> | ||
#include <err.h> | #include <err.h> | ||
|
|
||
#include <stdio.h> | #include <stdio.h> | ||
#include <stdlib.h> | #include <stdlib.h> | ||
#include <string.h> | #include <string.h> | ||
#ifndef __OpenBSD__ | #ifndef __OpenBSD__ | ||
#include <time.h> | #include <time.h> | ||
#endif | #endif | ||
|
|
||
#include "mmain.h" | #include "mmain.h" | ||
#include "term.h" | #include "term.h" | ||
#ifdef __NetBSD__ | |||
#define xisspace(x) isspace((int)(x)) | |||
#else | |||
#define xisspace(x) isspace((x)) | |||
#endif | |||
enum termstyle { | |||
STYLE_CLEAR, | |||
STYLE_BOLD, | |||
STYLE_UNDERLINE | |||
}; | |||
static void body(struct termp *, | static void body(struct termp *, | ||
struct termpair *, | |||
const struct mdoc_meta *, | const struct mdoc_meta *, | ||
const struct mdoc_node *); | const struct mdoc_node *); | ||
static void header(struct termp *, | static void header(struct termp *, | ||
|
|
||
const struct mdoc_meta *); | const struct mdoc_meta *); | ||
static void pword(struct termp *, const char *, size_t); | static void pword(struct termp *, const char *, size_t); | ||
static void pescape(struct termp *, | static void pescape(struct termp *, const char *, | ||
const char *, size_t *, size_t); | size_t *, size_t); | ||
static void style(struct termp *, enum tstyle); | |||
static void nescape(struct termp *, | |||
const char *, size_t); | |||
static void chara(struct termp *, char); | static void chara(struct termp *, char); | ||
static void stringa(struct termp *, const char *); | static void stringa(struct termp *, | ||
static void style(struct termp *, enum termstyle); | const char *, size_t); | ||
static void symbola(struct termp *, enum tsym); | |||
#ifdef __linux__ | #ifdef __linux__ | ||
extern size_t strlcat(char *, const char *, size_t); | extern size_t strlcat(char *, const char *, size_t); | ||
extern size_t strlcpy(char *, const char *, size_t); | extern size_t strlcpy(char *, const char *, size_t); | ||
#endif | #endif | ||
static struct termsym termsym_ansi[] = { | |||
{ "]", 1 }, /* TERMSYM_RBRACK */ | |||
{ "[", 1 }, /* TERMSYM_LBRACK */ | |||
{ "<-", 2 }, /* TERMSYM_LARROW */ | |||
{ "->", 2 }, /* TERMSYM_RARROW */ | |||
{ "^", 1 }, /* TERMSYM_UARROW */ | |||
{ "v", 1 }, /* TERMSYM_DARROW */ | |||
{ "`", 1 }, /* TERMSYM_LSQUOTE */ | |||
{ "\'", 1 }, /* TERMSYM_RSQUOTE */ | |||
{ "\'", 1 }, /* TERMSYM_SQUOTE */ | |||
{ "``", 2 }, /* TERMSYM_LDQUOTE */ | |||
{ "\'\'", 2 }, /* TERMSYM_RDQUOTE */ | |||
{ "\"", 1 }, /* TERMSYM_DQUOTE */ | |||
{ "<", 1 }, /* TERMSYM_LT */ | |||
{ ">", 1 }, /* TERMSYM_GT */ | |||
{ "<=", 2 }, /* TERMSYM_LE */ | |||
{ ">=", 2 }, /* TERMSYM_GE */ | |||
{ "==", 2 }, /* TERMSYM_EQ */ | |||
{ "!=", 2 }, /* TERMSYM_NEQ */ | |||
{ "\'", 1 }, /* TERMSYM_ACUTE */ | |||
{ "`", 1 }, /* TERMSYM_GRAVE */ | |||
{ "pi", 2 }, /* TERMSYM_PI */ | |||
{ "+=", 2 }, /* TERMSYM_PLUSMINUS */ | |||
{ "oo", 2 }, /* TERMSYM_INF */ | |||
{ "infinity", 8 }, /* TERMSYM_INF2 */ | |||
{ "NaN", 3 }, /* TERMSYM_NAN */ | |||
{ "|", 1 }, /* TERMSYM_BAR */ | |||
{ "o", 1 }, /* TERMSYM_BULLET */ | |||
{ "&", 1 }, /* TERMSYM_AND */ | |||
{ "|", 1 }, /* TERMSYM_OR */ | |||
}; | |||
static const char ansi_clear[] = { 27, '[', '0', 'm' }; | |||
static const char ansi_bold[] = { 27, '[', '1', 'm' }; | |||
static const char ansi_under[] = { 27, '[', '4', 'm' }; | |||
static struct termsym termstyle_ansi[] = { | |||
{ ansi_clear, 4 }, | |||
{ ansi_bold, 4 }, | |||
{ ansi_under, 4 } | |||
}; | |||
int | int | ||
main(int argc, char *argv[]) | main(int argc, char *argv[]) | ||
{ | { | ||
|
|
||
if (NULL == (mdoc = mmain_mdoc(p))) | if (NULL == (mdoc = mmain_mdoc(p))) | ||
mmain_exit(p, 1); | mmain_exit(p, 1); | ||
termp.maxrmargin = 80; /* XXX */ | termp.maxrmargin = 78; /* XXX */ | ||
termp.rmargin = termp.maxrmargin; | termp.rmargin = termp.maxrmargin; | ||
termp.maxcols = 1024; | termp.maxcols = 1024; | ||
termp.offset = termp.col = 0; | termp.offset = termp.col = 0; | ||
termp.flags = TERMP_NOSPACE; | termp.flags = TERMP_NOSPACE; | ||
termp.symtab = termsym_ansi; | |||
termp.styletab = termstyle_ansi; | |||
if (NULL == (termp.buf = malloc(termp.maxcols))) | if (NULL == (termp.buf = malloc(termp.maxcols))) | ||
err(1, "malloc"); | err(1, "malloc"); | ||
header(&termp, mdoc_meta(mdoc)); | header(&termp, mdoc_meta(mdoc)); | ||
body(&termp, mdoc_meta(mdoc), mdoc_node(mdoc)); | body(&termp, NULL, mdoc_meta(mdoc), mdoc_node(mdoc)); | ||
footer(&termp, mdoc_meta(mdoc)); | footer(&termp, mdoc_meta(mdoc)); | ||
free(termp.buf); | free(termp.buf); | ||
|
|
||
} | } | ||
/* | |||
* Flush a line of text. A "line" is loosely defined as being something | |||
* that should be followed by a newline, regardless of whether it's | |||
* broken apart by newlines getting there. A line can also be a | |||
* fragment of a columnar list. | |||
* | |||
* Specifically, a line is whatever's in p->buf of length p->col, which | |||
* is zeroed after this function returns. | |||
* | |||
* The variables TERMP_NOLPAD, TERMP_LITERAL and TERMP_NOBREAK are of | |||
* critical importance here. Their behaviour follows: | |||
* | |||
* - TERMP_NOLPAD: when beginning to write the line, don't left-pad the | |||
* offset value. This is useful when doing columnar lists where the | |||
* prior column has right-padded. | |||
* | |||
* - TERMP_LITERAL: don't break apart words. Note that a long literal | |||
* word will violate the right margin. | |||
* | |||
* - TERMP_NOBREAK: this is the most important and is used when making | |||
* columns. In short: don't print a newline and instead pad to the | |||
* right margin. Used in conjunction with TERMP_NOLPAD. | |||
* | |||
* In-line line breaking: | |||
* | |||
* If TERMP_NOBREAK is specified and the line overruns the right | |||
* margin, it will break and pad-right to the right margin after | |||
* writing. If maxrmargin is violated, it will break and continue | |||
* writing from the right-margin, which will lead to the above | |||
* scenario upon exit. | |||
* | |||
* Otherwise, the line will break at the right margin. Extremely long | |||
* lines will cause the system to emit a warning (TODO: hyphenate, if | |||
* possible). | |||
*/ | |||
void | void | ||
flushln(struct termp *p) | flushln(struct termp *p) | ||
{ | { | ||
size_t i, j, vsz, vis, maxvis; | size_t i, j, vsz, vis, maxvis, mmax, bp; | ||
/* | /* | ||
* First, establish the maximum columns of "visible" content. | * First, establish the maximum columns of "visible" content. | ||
|
|
||
assert(p->offset < p->rmargin); | assert(p->offset < p->rmargin); | ||
maxvis = p->rmargin - p->offset; | maxvis = p->rmargin - p->offset; | ||
mmax = p->maxrmargin - p->offset; | |||
bp = TERMP_NOBREAK & p->flags ? mmax : maxvis; | |||
vis = 0; | vis = 0; | ||
/* | /* | ||
|
|
||
for (j = 0; j < p->offset; j++) | for (j = 0; j < p->offset; j++) | ||
putchar(' '); | putchar(' '); | ||
/* | |||
* If we're literal, print out verbatim. | |||
*/ | |||
if (p->flags & TERMP_LITERAL) { | |||
/* FIXME: count non-printing chars. */ | |||
for (i = 0; i < p->col; i++) | |||
putchar(p->buf[i]); | |||
putchar('\n'); | |||
p->col = 0; | |||
return; | |||
} | |||
for (i = 0; i < p->col; i++) { | for (i = 0; i < p->col; i++) { | ||
/* | /* | ||
* Count up visible word characters. Control sequences | * Count up visible word characters. Control sequences | ||
* (starting with the CSI) aren't counted. | * (starting with the CSI) aren't counted. A space | ||
* generates a non-printing word, which is valid (the | |||
* space is printed according to regular spacing rules). | |||
*/ | */ | ||
assert( ! xisspace(p->buf[i])); | |||
/* FIXME: make non-ANSI friendly. */ | |||
/* LINTED */ | /* LINTED */ | ||
for (j = i, vsz = 0; j < p->col; j++) { | for (j = i, vsz = 0; j < p->col; j++) { | ||
if (xisspace(p->buf[j])) | if (isspace((int)p->buf[j])) | ||
break; | break; | ||
else if (27 == p->buf[j]) { | else if (27 == p->buf[j]) { | ||
assert(j + 4 <= p->col); | assert(j + 4 <= p->col); | ||
|
|
||
} else | } else | ||
vsz++; | vsz++; | ||
} | } | ||
assert(vsz > 0); | |||
/* | /* | ||
* If a word is too long and we're within a line, put it | * Do line-breaking. If we're greater than our | ||
* on the next line. Puke if we're being asked to write | * break-point and already in-line, break to the next | ||
* something that will exceed the right margin (i.e., | * line and start writing. If we're at the line start, | ||
* from a fresh line or when we're not allowed to break | * then write out the word (TODO: hyphenate) and break | ||
* the line with TERMP_NOBREAK). | * in a subsequent loop invocation. | ||
*/ | */ | ||
if (vis && vis + vsz >= maxvis) { | if ( ! (TERMP_NOBREAK & p->flags)) { | ||
/* FIXME */ | if (vis && vis + vsz > bp) { | ||
if (p->flags & TERMP_NOBREAK) | putchar('\n'); | ||
errx(1, "word breaks right margin"); | for (j = 0; j < p->offset; j++) | ||
putchar('\n'); | putchar(' '); | ||
for (j = 0; j < p->offset; j++) | vis = 0; | ||
putchar(' '); | } else if (vis + vsz > bp) | ||
vis = 0; | warnx("word breaks right margin"); | ||
} else if (vis + vsz >= maxvis) | |||
/* FIXME */ | |||
errx(1, "word breaks right margin"); | |||
/* TODO: hyphenate. */ | |||
} else { | |||
if (vis && vis + vsz > bp) { | |||
putchar('\n'); | |||
for (j = 0; j < p->rmargin; j++) | |||
putchar(' '); | |||
vis = p->rmargin; | |||
} else if (vis + vsz > bp) | |||
warnx("word breaks right margin"); | |||
/* TODO: hyphenate. */ | |||
} | |||
/* | /* | ||
* Write out the word and a trailing space. Omit the | * Write out the word and a trailing space. Omit the | ||
* space if we're the last word in the line. | * space if we're the last word in the line or beyond | ||
* our breakpoint. | |||
*/ | */ | ||
for ( ; i < p->col; i++) { | for ( ; i < p->col; i++) { | ||
if (xisspace(p->buf[i])) | if (isspace((int)p->buf[i])) | ||
break; | break; | ||
putchar(p->buf[i]); | putchar(p->buf[i]); | ||
} | } | ||
vis += vsz; | vis += vsz; | ||
if (i < p->col) { | if (i < p->col && vis <= bp) { | ||
putchar(' '); | putchar(' '); | ||
vis++; | vis++; | ||
} | } | ||
} | } | ||
/* | /* | ||
* If we've overstepped our maximum visible no-break space, then | |||
* cause a newline and offset at the right margin. | |||
*/ | |||
if ((TERMP_NOBREAK & p->flags) && vis >= maxvis) { | |||
putchar('\n'); | |||
for (i = 0; i < p->rmargin; i++) | |||
putchar(' '); | |||
p->col = 0; | |||
return; | |||
} | |||
/* | |||
* If we're not to right-marginalise it (newline), then instead | * If we're not to right-marginalise it (newline), then instead | ||
* pad to the right margin and stay off. | * pad to the right margin and stay off. | ||
*/ | */ | ||
if (p->flags & TERMP_NOBREAK) { | if (p->flags & TERMP_NOBREAK) { | ||
for ( ; vis <= maxvis; vis++) | for ( ; vis < maxvis; vis++) | ||
putchar(' '); | putchar(' '); | ||
} else | } else | ||
putchar('\n'); | putchar('\n'); | ||
|
|
||
} | } | ||
/* | |||
* A newline only breaks an existing line; it won't assert vertical | |||
* space. All data in the output buffer is flushed prior to the newline | |||
* assertion. | |||
*/ | |||
void | void | ||
newln(struct termp *p) | newln(struct termp *p) | ||
{ | { | ||
/* | |||
* A newline only breaks an existing line; it won't assert | |||
* vertical space. | |||
*/ | |||
p->flags |= TERMP_NOSPACE; | p->flags |= TERMP_NOSPACE; | ||
if (0 == p->col) | if (0 == p->col) { | ||
p->flags &= ~TERMP_NOLPAD; | |||
return; | return; | ||
} | |||
flushln(p); | flushln(p); | ||
p->flags &= ~TERMP_NOLPAD; | |||
} | } | ||
/* | |||
* Asserts a vertical space (a full, empty line-break between lines). | |||
* Note that if used twice, this will cause two blank spaces and so on. | |||
* All data in the output buffer is flushed prior to the newline | |||
* assertion. | |||
*/ | |||
void | void | ||
vspace(struct termp *p) | vspace(struct termp *p) | ||
{ | { | ||
/* | |||
* Asserts a vertical space (a full, empty line-break between | |||
* lines). | |||
*/ | |||
newln(p); | newln(p); | ||
putchar('\n'); | putchar('\n'); | ||
} | } | ||
static void | /* | ||
stringa(struct termp *p, const char *s) | * Break apart a word into "pwords" (partial-words, usually from | ||
{ | * breaking up a phrase into individual words) and, eventually, put them | ||
* into the output buffer. If we're a literal word, then don't break up | |||
/* XXX - speed up if not passing to chara. */ | * the word and put it verbatim into the output buffer. | ||
for ( ; *s; s++) | */ | ||
chara(p, *s); | |||
} | |||
static void | |||
chara(struct termp *p, char c) | |||
{ | |||
/* TODO: dynamically expand the buffer. */ | |||
if (p->col + 1 >= p->maxcols) | |||
errx(1, "line overrun"); | |||
p->buf[(p->col)++] = c; | |||
} | |||
static void | |||
style(struct termp *p, enum termstyle esc) | |||
{ | |||
if (p->col + 4 >= p->maxcols) | |||
errx(1, "line overrun"); | |||
p->buf[(p->col)++] = 27; | |||
p->buf[(p->col)++] = '['; | |||
switch (esc) { | |||
case (STYLE_CLEAR): | |||
p->buf[(p->col)++] = '0'; | |||
break; | |||
case (STYLE_BOLD): | |||
p->buf[(p->col)++] = '1'; | |||
break; | |||
case (STYLE_UNDERLINE): | |||
p->buf[(p->col)++] = '4'; | |||
break; | |||
default: | |||
abort(); | |||
/* NOTREACHED */ | |||
} | |||
p->buf[(p->col)++] = 'm'; | |||
} | |||
static void | |||
pescape(struct termp *p, const char *word, size_t *i, size_t len) | |||
{ | |||
(*i)++; | |||
assert(*i < len); | |||
if ('(' == word[*i]) { | |||
/* Two-character escapes. */ | |||
(*i)++; | |||
assert(*i + 1 < len); | |||
if ('r' == word[*i] && 'B' == word[*i + 1]) | |||
chara(p, ']'); | |||
else if ('l' == word[*i] && 'B' == word[*i + 1]) | |||
chara(p, '['); | |||
else if ('<' == word[*i] && '-' == word[*i + 1]) | |||
stringa(p, "<-"); | |||
else if ('-' == word[*i] && '>' == word[*i + 1]) | |||
stringa(p, "->"); | |||
(*i)++; | |||
return; | |||
} else if ('[' != word[*i]) { | |||
/* One-character escapes. */ | |||
switch (word[*i]) { | |||
case ('\\'): | |||
/* FALLTHROUGH */ | |||
case ('\''): | |||
/* FALLTHROUGH */ | |||
case ('`'): | |||
/* FALLTHROUGH */ | |||
case ('-'): | |||
/* FALLTHROUGH */ | |||
case ('.'): | |||
chara(p, word[*i]); | |||
default: | |||
break; | |||
} | |||
return; | |||
} | |||
/* n-character escapes. */ | |||
} | |||
static void | |||
pword(struct termp *p, const char *word, size_t len) | |||
{ | |||
size_t i; | |||
/*assert(len > 0);*/ /* Can be, if literal. */ | |||
if ( ! (p->flags & TERMP_NOSPACE) && | |||
! (p->flags & TERMP_LITERAL)) | |||
chara(p, ' '); | |||
p->flags &= ~TERMP_NOSPACE; | |||
if (p->flags & TERMP_BOLD) | |||
style(p, STYLE_BOLD); | |||
if (p->flags & TERMP_UNDERLINE) | |||
style(p, STYLE_UNDERLINE); | |||
for (i = 0; i < len; i++) { | |||
if ('\\' == word[i]) { | |||
pescape(p, word, &i, len); | |||
continue; | |||
} | |||
chara(p, word[i]); | |||
} | |||
if (p->flags & TERMP_BOLD || | |||
p->flags & TERMP_UNDERLINE) | |||
style(p, STYLE_CLEAR); | |||
} | |||
void | void | ||
word(struct termp *p, const char *word) | word(struct termp *p, const char *word) | ||
{ | { | ||
|
|
||
/* LINTED */ | /* LINTED */ | ||
for (j = i = 0; i < len; i++) { | for (j = i = 0; i < len; i++) { | ||
if ( ! xisspace(word[i])) { | if ( ! isspace((int)word[i])) { | ||
j++; | j++; | ||
continue; | continue; | ||
} | |||
/* Escaped spaces don't delimit... */ | |||
if (i > 0 && isspace((int)word[i]) && | |||
'\\' == word[i - 1]) { | |||
j++; | |||
continue; | |||
} | } | ||
if (0 == j) | if (0 == j) | ||
continue; | continue; | ||
assert(i >= j); | assert(i >= j); | ||
|
|
||
} | } | ||
/* | |||
* This is the main function for printing out nodes. It's constituted | |||
* of PRE and POST functions, which correspond to prefix and infix | |||
* processing. The termpair structure allows data to persist between | |||
* prefix and postfix invocations. | |||
*/ | |||
static void | static void | ||
body(struct termp *p, const struct mdoc_meta *meta, | body(struct termp *p, struct termpair *ppair, | ||
const struct mdoc_meta *meta, | |||
const struct mdoc_node *node) | const struct mdoc_node *node) | ||
{ | { | ||
int dochild; | int dochild; | ||
struct termpair pair; | |||
/* Pre-processing. */ | /* Pre-processing. */ | ||
dochild = 1; | dochild = 1; | ||
pair.ppair = ppair; | |||
pair.type = 0; | |||
pair.offset = pair.rmargin = 0; | |||
pair.flag = 0; | |||
pair.count = 0; | |||
if (MDOC_TEXT != node->type) { | if (MDOC_TEXT != node->type) { | ||
if (termacts[node->tok].pre) | if (termacts[node->tok].pre) | ||
if ( ! (*termacts[node->tok].pre)(p, meta, node)) | if ( ! (*termacts[node->tok].pre)(p, &pair, meta, node)) | ||
dochild = 0; | dochild = 0; | ||
} else /* MDOC_TEXT == node->type */ | } else /* MDOC_TEXT == node->type */ | ||
word(p, node->data.text.string); | word(p, node->data.text.string); | ||
/* Children. */ | /* Children. */ | ||
if (TERMPAIR_FLAG & pair.type) | |||
p->flags |= pair.flag; | |||
if (dochild && node->child) | if (dochild && node->child) | ||
body(p, meta, node->child); | body(p, &pair, meta, node->child); | ||
if (TERMPAIR_FLAG & pair.type) | |||
p->flags &= ~pair.flag; | |||
/* Post-processing. */ | /* Post-processing. */ | ||
if (MDOC_TEXT != node->type) | if (MDOC_TEXT != node->type) | ||
if (termacts[node->tok].post) | if (termacts[node->tok].post) | ||
(*termacts[node->tok].post)(p, meta, node); | (*termacts[node->tok].post)(p, &pair, meta, node); | ||
/* Siblings. */ | /* Siblings. */ | ||
if (node->next) | if (node->next) | ||
body(p, meta, node->next); | body(p, ppair, meta, node->next); | ||
} | } | ||
|
|
||
{ | { | ||
struct tm *tm; | struct tm *tm; | ||
char *buf, *os; | char *buf, *os; | ||
size_t sz, osz, ssz, i; | |||
if (NULL == (buf = malloc(p->rmargin))) | if (NULL == (buf = malloc(p->rmargin))) | ||
err(1, "malloc"); | err(1, "malloc"); | ||
|
|
||
#endif | #endif | ||
err(1, "strftime"); | err(1, "strftime"); | ||
osz = strlcpy(os, meta->os, p->rmargin); | (void)strlcpy(os, meta->os, p->rmargin); | ||
sz = strlen(buf); | /* | ||
ssz = sz + osz + 1; | * This is /slightly/ different from regular groff output | ||
* because we don't have page numbers. Print the following: | |||
* | |||
* OS MDOCDATE | |||
*/ | |||
if (ssz > p->rmargin) { | vspace(p); | ||
ssz -= p->rmargin; | |||
assert(ssz <= osz); | |||
os[osz - ssz] = 0; | |||
ssz = 1; | |||
} else | |||
ssz = p->rmargin - ssz + 1; | |||
printf("\n"); | p->flags |= TERMP_NOSPACE | TERMP_NOBREAK; | ||
printf("%s", os); | p->rmargin = p->maxrmargin - strlen(buf); | ||
for (i = 0; i < ssz; i++) | p->offset = 0; | ||
printf(" "); | |||
printf("%s\n", buf); | word(p, os); | ||
fflush(stdout); | flushln(p); | ||
p->flags |= TERMP_NOLPAD | TERMP_NOSPACE; | |||
p->offset = p->rmargin; | |||
p->rmargin = p->maxrmargin; | |||
p->flags &= ~TERMP_NOBREAK; | |||
word(p, buf); | |||
flushln(p); | |||
free(buf); | free(buf); | ||
free(os); | free(os); | ||
} | } | ||
|
|
||
static void | static void | ||
header(struct termp *p, const struct mdoc_meta *meta) | header(struct termp *p, const struct mdoc_meta *meta) | ||
{ | { | ||
char *buf, *title; | char *buf, *title, *bufp, *vbuf; | ||
const char *pp, *msec; | const char *pp; | ||
size_t ssz, tsz, ttsz, i;; | struct utsname uts; | ||
p->rmargin = p->maxrmargin; | |||
p->offset = 0; | |||
if (NULL == (buf = malloc(p->rmargin))) | if (NULL == (buf = malloc(p->rmargin))) | ||
err(1, "malloc"); | err(1, "malloc"); | ||
if (NULL == (title = malloc(p->rmargin))) | if (NULL == (title = malloc(p->rmargin))) | ||
err(1, "malloc"); | err(1, "malloc"); | ||
if (NULL == (vbuf = malloc(p->rmargin))) | |||
err(1, "malloc"); | |||
if (NULL == (pp = mdoc_vol2a(meta->vol))) | if (NULL == (pp = mdoc_vol2a(meta->vol))) { | ||
switch (meta->msec) { | switch (meta->msec) { | ||
case (MSEC_1): | case (MSEC_1): | ||
/* FALLTHROUGH */ | /* FALLTHROUGH */ | ||
|
|
||
pp = mdoc_vol2a(VOL_KM); | pp = mdoc_vol2a(VOL_KM); | ||
break; | break; | ||
default: | default: | ||
/* FIXME: capitalise. */ | |||
if (NULL == (pp = mdoc_msec2a(meta->msec))) | |||
pp = mdoc_msec2a(MSEC_local); | |||
break; | break; | ||
} | } | ||
assert(pp); | } | ||
vbuf[0] = 0; | |||
tsz = strlcpy(buf, pp, p->rmargin); | if (pp) { | ||
assert(tsz < p->rmargin); | if (-1 == uname(&uts)) | ||
err(1, "uname"); | |||
(void)strlcat(vbuf, uts.sysname, p->rmargin); | |||
(void)strlcat(vbuf, " ", p->rmargin); | |||
} else if (NULL == (pp = mdoc_msec2a(meta->msec))) | |||
pp = mdoc_msec2a(MSEC_local); | |||
if ((pp = mdoc_arch2a(meta->arch))) { | (void)strlcat(vbuf, pp, p->rmargin); | ||
tsz = strlcat(buf, " (", p->rmargin); | |||
assert(tsz < p->rmargin); | |||
tsz = strlcat(buf, pp, p->rmargin); | |||
assert(tsz < p->rmargin); | |||
tsz = strlcat(buf, ")", p->rmargin); | |||
assert(tsz < p->rmargin); | |||
} | |||
ttsz = strlcpy(title, meta->title, p->rmargin); | /* | ||
* The header is strange. It has three components, which are | |||
* really two with the first duplicated. It goes like this: | |||
* | |||
* IDENTIFIER TITLE IDENTIFIER | |||
* | |||
* The IDENTIFIER is NAME(SECTION), which is the command-name | |||
* (if given, or "unknown" if not) followed by the manual page | |||
* section. These are given in `Dt'. The TITLE is a free-form | |||
* string depending on the manual volume. If not specified, it | |||
* switches on the manual section. | |||
*/ | |||
if (NULL == (msec = mdoc_msec2a(meta->msec))) | if (mdoc_arch2a(meta->arch)) | ||
msec = ""; | (void)snprintf(buf, p->rmargin, "%s (%s)", | ||
vbuf, mdoc_arch2a(meta->arch)); | |||
else | |||
(void)strlcpy(buf, vbuf, p->rmargin); | |||
ssz = (2 * (ttsz + 2 + strlen(msec))) + tsz + 2; | pp = mdoc_msec2a(meta->msec); | ||
if (ssz > p->rmargin) { | (void)snprintf(title, p->rmargin, "%s(%s)", | ||
if ((ssz -= p->rmargin) % 2) | meta->title, pp ? pp : ""); | ||
ssz++; | |||
ssz /= 2; | for (bufp = title; *bufp; bufp++) | ||
*bufp = toupper(*bufp); | |||
assert(ssz <= ttsz); | p->offset = 0; | ||
title[ttsz - ssz] = 0; | p->rmargin = (p->maxrmargin - strlen(buf)) / 2; | ||
ssz = 1; | p->flags |= TERMP_NOBREAK | TERMP_NOSPACE; | ||
} else | |||
ssz = ((p->rmargin - ssz) / 2) + 1; | |||
printf("%s(%s)", title, msec); | word(p, title); | ||
flushln(p); | |||
for (i = 0; i < ssz; i++) | p->flags |= TERMP_NOLPAD | TERMP_NOSPACE; | ||
printf(" "); | p->offset = p->rmargin; | ||
p->rmargin = p->maxrmargin - strlen(title); | |||
printf("%s", buf); | word(p, buf); | ||
flushln(p); | |||
for (i = 0; i < ssz; i++) | p->offset = p->rmargin; | ||
printf(" "); | p->rmargin = p->maxrmargin; | ||
p->flags &= ~TERMP_NOBREAK; | |||
p->flags |= TERMP_NOLPAD | TERMP_NOSPACE; | |||
printf("%s(%s)\n", title, msec); | word(p, title); | ||
fflush(stdout); | flushln(p); | ||
p->rmargin = p->maxrmargin; | |||
p->offset = 0; | |||
p->flags &= ~TERMP_NOSPACE; | |||
free(title); | free(title); | ||
free(vbuf); | |||
free(buf); | free(buf); | ||
} | |||
/* | |||
* Determine the symbol indicated by an escape sequences, that is, one | |||
* starting with a backslash. Once done, we pass this value into the | |||
* output buffer by way of the symbol table. | |||
*/ | |||
static void | |||
nescape(struct termp *p, const char *word, size_t len) | |||
{ | |||
switch (len) { | |||
case (1): | |||
switch (word[0]) { | |||
case ('\\'): | |||
/* FALLTHROUGH */ | |||
case ('\''): | |||
/* FALLTHROUGH */ | |||
case ('`'): | |||
/* FALLTHROUGH */ | |||
case ('-'): | |||
/* FALLTHROUGH */ | |||
case (' '): | |||
/* FALLTHROUGH */ | |||
case ('.'): | |||
chara(p, word[0]); /* FIXME */ | |||
break; | |||
case ('&'): | |||
break; | |||
case ('e'): | |||
chara(p, '\\'); /* FIXME */ | |||
break; | |||
case ('q'): | |||
symbola(p, TERMSYM_DQUOTE); | |||
break; | |||
default: | |||
warnx("escape sequence not supported: %c", | |||
word[0]); | |||
break; | |||
} | |||
break; | |||
case (2): | |||
if ('r' == word[0] && 'B' == word[1]) | |||
symbola(p, TERMSYM_RBRACK); | |||
else if ('l' == word[0] && 'B' == word[1]) | |||
symbola(p, TERMSYM_LBRACK); | |||
else if ('l' == word[0] && 'q' == word[1]) | |||
symbola(p, TERMSYM_LDQUOTE); | |||
else if ('r' == word[0] && 'q' == word[1]) | |||
symbola(p, TERMSYM_RDQUOTE); | |||
else if ('o' == word[0] && 'q' == word[1]) | |||
symbola(p, TERMSYM_LSQUOTE); | |||
else if ('a' == word[0] && 'q' == word[1]) | |||
symbola(p, TERMSYM_RSQUOTE); | |||
else if ('<' == word[0] && '-' == word[1]) | |||
symbola(p, TERMSYM_LARROW); | |||
else if ('-' == word[0] && '>' == word[1]) | |||
symbola(p, TERMSYM_RARROW); | |||
else if ('b' == word[0] && 'u' == word[1]) | |||
symbola(p, TERMSYM_BULLET); | |||
else if ('<' == word[0] && '=' == word[1]) | |||
symbola(p, TERMSYM_LE); | |||
else if ('>' == word[0] && '=' == word[1]) | |||
symbola(p, TERMSYM_GE); | |||
else if ('=' == word[0] && '=' == word[1]) | |||
symbola(p, TERMSYM_EQ); | |||
else if ('+' == word[0] && '-' == word[1]) | |||
symbola(p, TERMSYM_PLUSMINUS); | |||
else if ('u' == word[0] && 'a' == word[1]) | |||
symbola(p, TERMSYM_UARROW); | |||
else if ('d' == word[0] && 'a' == word[1]) | |||
symbola(p, TERMSYM_DARROW); | |||
else if ('a' == word[0] && 'a' == word[1]) | |||
symbola(p, TERMSYM_ACUTE); | |||
else if ('g' == word[0] && 'a' == word[1]) | |||
symbola(p, TERMSYM_GRAVE); | |||
else if ('!' == word[0] && '=' == word[1]) | |||
symbola(p, TERMSYM_NEQ); | |||
else if ('i' == word[0] && 'f' == word[1]) | |||
symbola(p, TERMSYM_INF); | |||
else if ('n' == word[0] && 'a' == word[1]) | |||
symbola(p, TERMSYM_NAN); | |||
else if ('b' == word[0] && 'a' == word[1]) | |||
symbola(p, TERMSYM_BAR); | |||
/* Deprecated forms. */ | |||
else if ('A' == word[0] && 'm' == word[1]) | |||
symbola(p, TERMSYM_AMP); | |||
else if ('B' == word[0] && 'a' == word[1]) | |||
symbola(p, TERMSYM_BAR); | |||
else if ('I' == word[0] && 'f' == word[1]) | |||
symbola(p, TERMSYM_INF2); | |||
else if ('G' == word[0] && 'e' == word[1]) | |||
symbola(p, TERMSYM_GE); | |||
else if ('G' == word[0] && 't' == word[1]) | |||
symbola(p, TERMSYM_GT); | |||
else if ('L' == word[0] && 'e' == word[1]) | |||
symbola(p, TERMSYM_LE); | |||
else if ('L' == word[0] && 'q' == word[1]) | |||
symbola(p, TERMSYM_LDQUOTE); | |||
else if ('L' == word[0] && 't' == word[1]) | |||
symbola(p, TERMSYM_LT); | |||
else if ('N' == word[0] && 'a' == word[1]) | |||
symbola(p, TERMSYM_NAN); | |||
else if ('N' == word[0] && 'e' == word[1]) | |||
symbola(p, TERMSYM_NEQ); | |||
else if ('P' == word[0] && 'i' == word[1]) | |||
symbola(p, TERMSYM_PI); | |||
else if ('P' == word[0] && 'm' == word[1]) | |||
symbola(p, TERMSYM_PLUSMINUS); | |||
else if ('R' == word[0] && 'q' == word[1]) | |||
symbola(p, TERMSYM_RDQUOTE); | |||
else | |||
warnx("escape sequence not supported: %c%c", | |||
word[0], word[1]); | |||
break; | |||
default: | |||
warnx("escape sequence not supported"); | |||
break; | |||
} | |||
} | |||
/* | |||
* Apply a style to the output buffer. This is looked up by means of | |||
* the styletab. | |||
*/ | |||
static void | |||
style(struct termp *p, enum tstyle esc) | |||
{ | |||
if (p->col + 4 >= p->maxcols) | |||
errx(1, "line overrun"); | |||
p->buf[(p->col)++] = 27; | |||
p->buf[(p->col)++] = '['; | |||
switch (esc) { | |||
case (TERMSTYLE_CLEAR): | |||
p->buf[(p->col)++] = '0'; | |||
break; | |||
case (TERMSTYLE_BOLD): | |||
p->buf[(p->col)++] = '1'; | |||
break; | |||
case (TERMSTYLE_UNDER): | |||
p->buf[(p->col)++] = '4'; | |||
break; | |||
default: | |||
abort(); | |||
/* NOTREACHED */ | |||
} | |||
p->buf[(p->col)++] = 'm'; | |||
} | |||
/* | |||
* Handle an escape sequence: determine its length and pass it to the | |||
* escape-symbol look table. Note that we assume mdoc(3) has validated | |||
* the escape sequence (we assert upon badly-formed escape sequences). | |||
*/ | |||
static void | |||
pescape(struct termp *p, const char *word, size_t *i, size_t len) | |||
{ | |||
size_t j; | |||
(*i)++; | |||
assert(*i < len); | |||
if ('(' == word[*i]) { | |||
(*i)++; | |||
assert(*i + 1 < len); | |||
nescape(p, &word[*i], 2); | |||
(*i)++; | |||
return; | |||
} else if ('*' == word[*i]) { | |||
/* XXX - deprecated! */ | |||
(*i)++; | |||
assert(*i < len); | |||
switch (word[*i]) { | |||
case ('('): | |||
(*i)++; | |||
assert(*i + 1 < len); | |||
nescape(p, &word[*i], 2); | |||
(*i)++; | |||
return; | |||
case ('['): | |||
break; | |||
default: | |||
nescape(p, &word[*i], 1); | |||
return; | |||
} | |||
} else if ('[' != word[*i]) { | |||
nescape(p, &word[*i], 1); | |||
return; | |||
} | |||
(*i)++; | |||
for (j = 0; word[*i] && ']' != word[*i]; (*i)++, j++) | |||
/* Loop... */ ; | |||
assert(word[*i]); | |||
nescape(p, &word[*i - j], j); | |||
} | |||
/* | |||
* Handle pwords, partial words, which may be either a single word or a | |||
* phrase that cannot be broken down (such as a literal string). This | |||
* handles word styling. | |||
*/ | |||
static void | |||
pword(struct termp *p, const char *word, size_t len) | |||
{ | |||
size_t i; | |||
if ( ! (TERMP_NOSPACE & p->flags) && | |||
! (TERMP_LITERAL & p->flags)) | |||
chara(p, ' '); | |||
if ( ! (p->flags & TERMP_NONOSPACE)) | |||
p->flags &= ~TERMP_NOSPACE; | |||
/* | |||
* XXX - if literal and underlining, this will underline the | |||
* spaces between literal words. | |||
*/ | |||
if (p->flags & TERMP_BOLD) | |||
style(p, TERMSTYLE_BOLD); | |||
if (p->flags & TERMP_UNDERLINE) | |||
style(p, TERMSTYLE_UNDER); | |||
for (i = 0; i < len; i++) { | |||
if ('\\' == word[i]) { | |||
pescape(p, word, &i, len); | |||
continue; | |||
} | |||
chara(p, word[i]); | |||
} | |||
if (p->flags & TERMP_BOLD || | |||
p->flags & TERMP_UNDERLINE) | |||
style(p, TERMSTYLE_CLEAR); | |||
} | |||
/* | |||
* Add a symbol to the output line buffer. | |||
*/ | |||
static void | |||
symbola(struct termp *p, enum tsym sym) | |||
{ | |||
assert(p->symtab[sym].sym); | |||
stringa(p, p->symtab[sym].sym, p->symtab[sym].sz); | |||
} | |||
/* | |||
* Like chara() but for arbitrary-length buffers. Resize the buffer by | |||
* a factor of two (if the buffer is less than that) or the buffer's | |||
* size. | |||
*/ | |||
static void | |||
stringa(struct termp *p, const char *c, size_t sz) | |||
{ | |||
size_t s; | |||
s = sz > p->maxcols * 2 ? sz : p->maxcols * 2; | |||
assert(c); | |||
if (p->col + sz >= p->maxcols) { | |||
p->buf = realloc(p->buf, s); | |||
if (NULL == p->buf) | |||
err(1, "realloc"); | |||
p->maxcols = s; | |||
} | |||
(void)memcpy(&p->buf[p->col], c, sz); | |||
p->col += sz; | |||
} | |||
/* | |||
* Insert a single character into the line-buffer. If the buffer's | |||
* space is exceeded, then allocate more space by doubling the buffer | |||
* size. | |||
*/ | |||
static void | |||
chara(struct termp *p, char c) | |||
{ | |||
if (p->col + 1 >= p->maxcols) { | |||
p->buf = realloc(p->buf, p->maxcols * 2); | |||
if (NULL == p->buf) | |||
err(1, "malloc"); | |||
p->maxcols *= 2; | |||
} | |||
p->buf[(p->col)++] = c; | |||
} | } |