Return to mdocterm.c CVS log | Up to [cvsweb.bsd.lv] / mandoc |
version 1.12, 2009/02/25 15:12:26 | version 1.42, 2009/03/14 12:35:02 | ||
---|---|---|---|
|
|
||
* 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/types.h> | |||
#include <assert.h> | #include <assert.h> | ||
#include <ctype.h> | #include <ctype.h> | ||
#include <err.h> | #include <err.h> | ||
|
|
||
#include <stdlib.h> | #include <stdlib.h> | ||
#include <string.h> | #include <string.h> | ||
#ifndef __OpenBSD__ | |||
#include <time.h> | |||
#endif | |||
#include "mmain.h" | #include "mmain.h" | ||
#include "term.h" | #include "term.h" | ||
#ifdef __NetBSD__ | struct termseq { | ||
#define xisspace(x) isspace((int)(x)) | const char *enc; | ||
#else | int sym; | ||
#define xisspace(x) isspace((x)) | |||
#endif | |||
enum termstyle { | |||
STYLE_CLEAR, | |||
STYLE_BOLD, | |||
STYLE_UNDERLINE | |||
}; | }; | ||
static int option(void *, int, const char *); | |||
static void body(struct termp *, | static void body(struct termp *, | ||
struct termpair *, | struct termpair *, | ||
const struct mdoc_meta *, | const struct mdoc_meta *, | ||
|
|
||
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 nescape(struct termp *, | static void nescape(struct termp *, | ||
const char *, size_t); | 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); | |||
static void sanity(const struct mdoc_node *); | |||
#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 termseq termenc1[] = { | |||
{ "\\", TERMSYM_SLASH }, | |||
{ "\'", TERMSYM_RSQUOTE }, | |||
{ "`", TERMSYM_LSQUOTE }, | |||
{ "-", TERMSYM_HYPHEN }, | |||
{ " ", TERMSYM_SPACE }, | |||
{ ".", TERMSYM_PERIOD }, | |||
{ "&", TERMSYM_BREAK }, | |||
{ "e", TERMSYM_SLASH }, | |||
{ "q", TERMSYM_DQUOTE }, | |||
{ "|", TERMSYM_BREAK }, | |||
{ NULL, 0 } | |||
}; | |||
static struct termseq termenc2[] = { | |||
{ "rC", TERMSYM_RBRACE }, | |||
{ "lC", TERMSYM_LBRACE }, | |||
{ "rB", TERMSYM_RBRACK }, | |||
{ "lB", TERMSYM_LBRACK }, | |||
{ "ra", TERMSYM_RANGLE }, | |||
{ "la", TERMSYM_LANGLE }, | |||
{ "Lq", TERMSYM_LDQUOTE }, | |||
{ "lq", TERMSYM_LDQUOTE }, | |||
{ "Rq", TERMSYM_RDQUOTE }, | |||
{ "rq", TERMSYM_RDQUOTE }, | |||
{ "oq", TERMSYM_LSQUOTE }, | |||
{ "aq", TERMSYM_RSQUOTE }, | |||
{ "<-", TERMSYM_LARROW }, | |||
{ "->", TERMSYM_RARROW }, | |||
{ "ua", TERMSYM_UARROW }, | |||
{ "da", TERMSYM_DARROW }, | |||
{ "bu", TERMSYM_BULLET }, | |||
{ "Ba", TERMSYM_BAR }, | |||
{ "ba", TERMSYM_BAR }, | |||
{ "co", TERMSYM_COPY }, | |||
{ "Am", TERMSYM_AMP }, | |||
{ "Le", TERMSYM_LE }, | |||
{ "<=", TERMSYM_LE }, | |||
{ "Ge", TERMSYM_GE }, | |||
{ ">=", TERMSYM_GE }, | |||
{ "==", TERMSYM_EQ }, | |||
{ "Ne", TERMSYM_NEQ }, | |||
{ "!=", TERMSYM_NEQ }, | |||
{ "Pm", TERMSYM_PLUSMINUS }, | |||
{ "+-", TERMSYM_PLUSMINUS }, | |||
{ "If", TERMSYM_INF2 }, | |||
{ "if", TERMSYM_INF }, | |||
{ "Na", TERMSYM_NAN }, | |||
{ "na", TERMSYM_NAN }, | |||
{ "**", TERMSYM_ASTERISK }, | |||
{ "Gt", TERMSYM_GT }, | |||
{ "Lt", TERMSYM_LT }, | |||
{ "aa", TERMSYM_ACUTE }, | |||
{ "ga", TERMSYM_GRAVE }, | |||
{ "en", TERMSYM_EN }, | |||
{ "em", TERMSYM_EM }, | |||
{ "Pi", TERMSYM_PI }, | |||
{ NULL, 0 } | |||
}; | |||
static struct termsym termsym_ascii[TERMSYM_MAX] = { | |||
{ "]", 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_AMP */ | |||
{ "--", 2 }, /* TERMSYM_EM */ | |||
{ "-", 1 }, /* TERMSYM_EN */ | |||
{ "(C)", 3 }, /* TERMSYM_COPY */ | |||
{ "*", 1 }, /* TERMSYM_ASTERISK */ | |||
{ "\\", 1 }, /* TERMSYM_SLASH */ | |||
{ "-", 1 }, /* TERMSYM_HYPHEN */ | |||
{ " ", 1 }, /* TERMSYM_SPACE */ | |||
{ ".", 1 }, /* TERMSYM_PERIOD */ | |||
{ "", 0 }, /* TERMSYM_BREAK */ | |||
{ "<", 1 }, /* TERMSYM_LANGLE */ | |||
{ ">", 1 }, /* TERMSYM_RANGLE */ | |||
{ "{", 1 }, /* TERMSYM_LBRACE */ | |||
{ "}", 1 }, /* TERMSYM_RBRACE */ | |||
}; | |||
int | int | ||
main(int argc, char *argv[]) | main(int argc, char *argv[]) | ||
{ | { | ||
struct mmain *p; | struct mmain *p; | ||
int c; | |||
const struct mdoc *mdoc; | const struct mdoc *mdoc; | ||
struct termp termp; | struct termp termp; | ||
(void)memset(&termp, 0, sizeof(struct termp)); | |||
termp.maxrmargin = termp.rmargin = 78; /* XXX */ | |||
termp.maxcols = 1024; | |||
termp.offset = termp.col = 0; | |||
termp.flags = TERMP_NOSPACE; | |||
termp.symtab = termsym_ascii; | |||
termp.enc = TERMENC_ANSI; | |||
p = mmain_alloc(); | p = mmain_alloc(); | ||
c = mmain_getopt(p, argc, argv, "[-Ooption...]", | |||
"O:", &termp, option); | |||
if ( ! mmain_getopt(p, argc, argv, NULL, NULL, NULL, NULL)) | if (1 != c) | ||
mmain_exit(p, 1); | mmain_exit(p, -1 == c ? 1 : 0); | ||
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.rmargin = termp.maxrmargin; | |||
termp.maxcols = 1024; | |||
termp.offset = termp.col = 0; | |||
termp.flags = TERMP_NOSPACE; | |||
if (NULL == (termp.buf = malloc(termp.maxcols))) | if (NULL == (termp.buf = malloc(termp.maxcols))) | ||
err(1, "malloc"); | err(1, "malloc"); | ||
|
|
||
} | } | ||
int | |||
option(void *ptr, int c, const char *arg) | |||
{ | |||
struct termp *p; | |||
p = (struct termp *)ptr; | |||
if (0 == strcmp(arg, "nroff")) { | |||
p->enc = TERMENC_NROFF; | |||
return(1); | |||
} else if (0 == strcmp(arg, "ansi")) { | |||
p->enc = TERMENC_ANSI; | |||
return(1); | |||
} | |||
warnx("unknown option: -O%s", arg); | |||
return(0); | |||
} | |||
/* | |||
* 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_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])); | |||
/* 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((u_char)p->buf[j])) { | ||
break; | break; | ||
else if (27 == p->buf[j]) { | } else if (27 == p->buf[j]) { | ||
assert(j + 4 <= p->col); | assert(TERMENC_ANSI == p->enc); | ||
j += 3; | assert(j + 5 <= p->col); | ||
j += 4; | |||
} else if (8 == p->buf[j]) { | |||
assert(TERMENC_NROFF == p->enc); | |||
assert(j + 2 <= p->col); | |||
j += 1; | |||
} 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 - p->offset; | |||
} 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((u_char)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) { | |||
if ( ! (TERMP_NONOBREAK & p->flags)) { | |||
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++) | if ( ! (TERMP_NONOBREAK & p->flags)) | ||
putchar(' '); | for ( ; vis < maxvis; vis++) | ||
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; | 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 | |||
nescape(struct termp *p, const char *word, size_t len) | |||
{ | |||
switch (len) { | |||
case (2): | |||
if ('r' == word[0] && 'B' == word[1]) | |||
chara(p, ']'); | |||
else if ('l' == word[0] && 'B' == word[1]) | |||
chara(p, '['); | |||
else if ('<' == word[0] && '-' == word[1]) | |||
stringa(p, "<-"); | |||
else if ('-' == word[0] && '>' == word[1]) | |||
stringa(p, "->"); | |||
else if ('l' == word[0] && 'q' == word[1]) | |||
chara(p, '\"'); | |||
else if ('r' == word[0] && 'q' == word[1]) | |||
chara(p, '\"'); | |||
else if ('b' == word[0] && 'u' == word[1]) | |||
chara(p, 'o'); | |||
break; | |||
default: | |||
break; | |||
} | |||
} | |||
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]) { | |||
/* Two-character escapes. */ | |||
(*i)++; | |||
assert(*i + 1 < len); | |||
nescape(p, &word[*i], 2); | |||
(*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; | |||
} | |||
(*i)++; | |||
for (j = 0; word[*i] && ']' != word[*i]; (*i)++, j++) | |||
/* Loop... */ ; | |||
nescape(p, &word[*i - j], j); | |||
} | |||
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) | ||
{ | { | ||
|
|
||
return; | return; | ||
} | } | ||
len = strlen(word); | if (0 == (len = strlen(word))) | ||
assert(len > 0); | errx(1, "blank line not in literal context"); | ||
if (mdoc_isdelim(word)) { | if (mdoc_isdelim(word)) { | ||
if ( ! (p->flags & TERMP_IGNDELIM)) | if ( ! (p->flags & TERMP_IGNDELIM)) | ||
|
|
||
/* LINTED */ | /* LINTED */ | ||
for (j = i = 0; i < len; i++) { | for (j = i = 0; i < len; i++) { | ||
if ( ! xisspace(word[i])) { | if ( ! isspace((u_char)word[i])) { | ||
j++; | j++; | ||
continue; | continue; | ||
} | |||
/* Escaped spaces don't delimit... */ | |||
if (i > 0 && isspace((u_char)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, struct termpair *ppair, | body(struct termp *p, struct termpair *ppair, | ||
const struct mdoc_meta *meta, | const struct mdoc_meta *meta, | ||
|
|
||
int dochild; | int dochild; | ||
struct termpair pair; | struct termpair pair; | ||
/* Some quick sanity-checking. */ | |||
sanity(node); | |||
/* Pre-processing. */ | /* Pre-processing. */ | ||
dochild = 1; | dochild = 1; | ||
|
|
||
if ( ! (*termacts[node->tok].pre)(p, &pair, 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->string); | ||
/* Children. */ | /* Children. */ | ||
|
|
||
{ | { | ||
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; | ||
const char *pp, *msec; | |||
size_t ssz, tsz, ttsz, i;; | |||
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 == (pp = mdoc_vol2a(meta->vol))) | /* | ||
switch (meta->msec) { | * The header is strange. It has three components, which are | ||
case (MSEC_1): | * really two with the first duplicated. It goes like this: | ||
/* FALLTHROUGH */ | * | ||
case (MSEC_6): | * IDENTIFIER TITLE IDENTIFIER | ||
/* FALLTHROUGH */ | * | ||
case (MSEC_7): | * The IDENTIFIER is NAME(SECTION), which is the command-name | ||
pp = mdoc_vol2a(VOL_URM); | * (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. | |||
*/ | |||
assert(meta->vol); | |||
(void)strlcpy(buf, meta->vol, p->rmargin); | |||
if (meta->arch) { | |||
(void)strlcat(buf, " (", p->rmargin); | |||
(void)strlcat(buf, meta->arch, p->rmargin); | |||
(void)strlcat(buf, ")", p->rmargin); | |||
} | |||
(void)snprintf(title, p->rmargin, "%s(%d)", | |||
meta->title, meta->msec); | |||
for (bufp = title; *bufp; bufp++) | |||
*bufp = toupper((u_char)*bufp); | |||
p->offset = 0; | |||
p->rmargin = (p->maxrmargin - strlen(buf)) / 2; | |||
p->flags |= TERMP_NOBREAK | TERMP_NOSPACE; | |||
word(p, title); | |||
flushln(p); | |||
p->flags |= TERMP_NOLPAD | TERMP_NOSPACE; | |||
p->offset = p->rmargin; | |||
p->rmargin = p->maxrmargin - strlen(title); | |||
word(p, buf); | |||
flushln(p); | |||
p->offset = p->rmargin; | |||
p->rmargin = p->maxrmargin; | |||
p->flags &= ~TERMP_NOBREAK; | |||
p->flags |= TERMP_NOLPAD | TERMP_NOSPACE; | |||
word(p, title); | |||
flushln(p); | |||
p->rmargin = p->maxrmargin; | |||
p->offset = 0; | |||
p->flags &= ~TERMP_NOSPACE; | |||
free(title); | |||
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) | |||
{ | |||
struct termseq *enc; | |||
switch (len) { | |||
case (1): | |||
enc = termenc1; | |||
break; | |||
case (2): | |||
enc = termenc2; | |||
break; | |||
default: | |||
warnx("unsupported %zu-byte escape sequence", len); | |||
return; | |||
} | |||
for ( ; enc->enc; enc++) | |||
if (0 == memcmp(enc->enc, word, len)) { | |||
symbola(p, enc->sym); | |||
return; | |||
} | |||
warnx("unsupported %zu-byte escape sequence", len); | |||
} | |||
/* | |||
* 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; | |||
if (++(*i) >= len) { | |||
warnx("ignoring bad escape sequence"); | |||
return; | |||
} | |||
if ('(' == word[*i]) { | |||
(*i)++; | |||
if (*i + 1 >= len) { | |||
warnx("ignoring bad escape sequence"); | |||
return; | |||
} | |||
nescape(p, &word[*i], 2); | |||
(*i)++; | |||
return; | |||
} else if ('*' == word[*i]) { | |||
(*i)++; | |||
if (*i >= len) { | |||
warnx("ignoring bad escape sequence"); | |||
return; | |||
} | |||
switch (word[*i]) { | |||
case ('('): | |||
(*i)++; | |||
if (*i + 1 >= len) { | |||
warnx("ignoring bad escape sequence"); | |||
return; | |||
} | |||
nescape(p, &word[*i], 2); | |||
(*i)++; | |||
return; | |||
case ('['): | |||
break; | break; | ||
case (MSEC_8): | |||
pp = mdoc_vol2a(VOL_SMM); | |||
break; | |||
case (MSEC_2): | |||
/* FALLTHROUGH */ | |||
case (MSEC_3): | |||
/* FALLTHROUGH */ | |||
case (MSEC_4): | |||
/* FALLTHROUGH */ | |||
case (MSEC_5): | |||
pp = mdoc_vol2a(VOL_PRM); | |||
break; | |||
case (MSEC_9): | |||
pp = mdoc_vol2a(VOL_KM); | |||
break; | |||
default: | default: | ||
/* FIXME: capitalise. */ | nescape(p, &word[*i], 1); | ||
if (NULL == (pp = mdoc_msec2a(meta->msec))) | return; | ||
pp = mdoc_msec2a(MSEC_local); | |||
break; | |||
} | } | ||
assert(pp); | |||
tsz = strlcpy(buf, pp, p->rmargin); | } else if ('[' != word[*i]) { | ||
assert(tsz < p->rmargin); | nescape(p, &word[*i], 1); | ||
return; | |||
} | |||
if ((pp = mdoc_arch2a(meta->arch))) { | (*i)++; | ||
tsz = strlcat(buf, " (", p->rmargin); | for (j = 0; word[*i] && ']' != word[*i]; (*i)++, j++) | ||
assert(tsz < p->rmargin); | /* Loop... */ ; | ||
tsz = strlcat(buf, pp, p->rmargin); | |||
assert(tsz < p->rmargin); | if (0 == word[*i]) { | ||
tsz = strlcat(buf, ")", p->rmargin); | warnx("ignoring bad escape sequence"); | ||
assert(tsz < p->rmargin); | return; | ||
} | } | ||
nescape(p, &word[*i - j], j); | |||
} | |||
ttsz = strlcpy(title, meta->title, p->rmargin); | |||
if (NULL == (msec = mdoc_msec2a(meta->msec))) | /* | ||
msec = ""; | * 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; | |||
ssz = (2 * (ttsz + 2 + strlen(msec))) + tsz + 2; | if ( ! (TERMP_NOSPACE & p->flags) && | ||
! (TERMP_LITERAL & p->flags)) | |||
chara(p, ' '); | |||
if (ssz > p->rmargin) { | if ( ! (p->flags & TERMP_NONOSPACE)) | ||
if ((ssz -= p->rmargin) % 2) | p->flags &= ~TERMP_NOSPACE; | ||
ssz++; | |||
ssz /= 2; | /* | ||
* If ANSI (word-length styling), then apply our style now, | |||
* before the word. | |||
*/ | |||
if (TERMENC_ANSI == p->enc && TERMP_STYLE & p->flags) { | |||
if (TERMP_BOLD & p->flags) { | |||
chara(p, 27); | |||
stringa(p, "[01m", 4); | |||
} | |||
if (TERMP_UNDER & p->flags) { | |||
chara(p, 27); | |||
stringa(p, "[04m", 4); | |||
} | |||
if (TERMP_RED & p->flags) { | |||
chara(p, 27); | |||
stringa(p, "[31m", 4); | |||
} | |||
if (TERMP_GREEN & p->flags) { | |||
chara(p, 27); | |||
stringa(p, "[32m", 4); | |||
} | |||
if (TERMP_YELLOW & p->flags) { | |||
chara(p, 27); | |||
stringa(p, "[33m", 4); | |||
} | |||
if (TERMP_BLUE & p->flags) { | |||
chara(p, 27); | |||
stringa(p, "[34m", 4); | |||
} | |||
if (TERMP_MAGENTA & p->flags) { | |||
chara(p, 27); | |||
stringa(p, "[35m", 4); | |||
} | |||
if (TERMP_CYAN & p->flags) { | |||
chara(p, 27); | |||
stringa(p, "[36m", 4); | |||
} | |||
} | |||
for (i = 0; i < len; i++) { | |||
if ('\\' == word[i]) { | |||
pescape(p, word, &i, len); | |||
continue; | |||
} | |||
if (TERMENC_NROFF == p->enc && | |||
TERMP_STYLE & p->flags) { | |||
if (TERMP_BOLD & p->flags) { | |||
chara(p, word[i]); | |||
chara(p, 8); | |||
} | |||
if (TERMP_UNDER & p->flags) { | |||
chara(p, '_'); | |||
chara(p, 8); | |||
} | |||
} | |||
chara(p, word[i]); | |||
} | |||
if (TERMENC_ANSI == p->enc && TERMP_STYLE & p->flags) { | |||
chara(p, 27); | |||
stringa(p, "[00m", 4); | |||
} | |||
} | |||
/* | |||
* 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; | |||
if (0 == sz) | |||
return; | |||
s = sz > p->maxcols * 2 ? sz : p->maxcols * 2; | |||
assert(ssz <= ttsz); | assert(c); | ||
title[ttsz - ssz] = 0; | if (p->col + sz >= p->maxcols) { | ||
ssz = 1; | p->buf = realloc(p->buf, s); | ||
} else | if (NULL == p->buf) | ||
ssz = ((p->rmargin - ssz) / 2) + 1; | err(1, "realloc"); | ||
p->maxcols = s; | |||
} | |||
printf("%s(%s)", title, msec); | (void)memcpy(&p->buf[p->col], c, sz); | ||
p->col += sz; | |||
} | |||
for (i = 0; i < ssz; i++) | |||
printf(" "); | |||
printf("%s", buf); | /* | ||
* 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) | |||
{ | |||
for (i = 0; i < ssz; i++) | if (p->col + 1 >= p->maxcols) { | ||
printf(" "); | p->buf = realloc(p->buf, p->maxcols * 2); | ||
if (NULL == p->buf) | |||
err(1, "malloc"); | |||
p->maxcols *= 2; | |||
} | |||
p->buf[(p->col)++] = c; | |||
} | |||
printf("%s(%s)\n", title, msec); | |||
fflush(stdout); | |||
free(title); | static void | ||
free(buf); | sanity(const struct mdoc_node *n) | ||
{ | |||
switch (n->type) { | |||
case (MDOC_TEXT): | |||
if (n->child) | |||
errx(1, "regular form violated (1)"); | |||
if (NULL == n->parent) | |||
errx(1, "regular form violated (2)"); | |||
if (NULL == n->string) | |||
errx(1, "regular form violated (3)"); | |||
switch (n->parent->type) { | |||
case (MDOC_TEXT): | |||
/* FALLTHROUGH */ | |||
case (MDOC_ROOT): | |||
errx(1, "regular form violated (4)"); | |||
/* NOTREACHED */ | |||
default: | |||
break; | |||
} | |||
break; | |||
case (MDOC_ELEM): | |||
if (NULL == n->parent) | |||
errx(1, "regular form violated (5)"); | |||
switch (n->parent->type) { | |||
case (MDOC_TAIL): | |||
/* FALLTHROUGH */ | |||
case (MDOC_BODY): | |||
/* FALLTHROUGH */ | |||
case (MDOC_HEAD): | |||
break; | |||
default: | |||
errx(1, "regular form violated (6)"); | |||
/* NOTREACHED */ | |||
} | |||
if (n->child) switch (n->child->type) { | |||
case (MDOC_TEXT): | |||
break; | |||
default: | |||
errx(1, "regular form violated (7("); | |||
/* NOTREACHED */ | |||
} | |||
break; | |||
case (MDOC_HEAD): | |||
/* FALLTHROUGH */ | |||
case (MDOC_BODY): | |||
/* FALLTHROUGH */ | |||
case (MDOC_TAIL): | |||
if (NULL == n->parent) | |||
errx(1, "regular form violated (8)"); | |||
if (MDOC_BLOCK != n->parent->type) | |||
errx(1, "regular form violated (9)"); | |||
if (n->child) switch (n->child->type) { | |||
case (MDOC_BLOCK): | |||
/* FALLTHROUGH */ | |||
case (MDOC_ELEM): | |||
/* FALLTHROUGH */ | |||
case (MDOC_TEXT): | |||
break; | |||
default: | |||
errx(1, "regular form violated (a)"); | |||
/* NOTREACHED */ | |||
} | |||
break; | |||
case (MDOC_BLOCK): | |||
if (NULL == n->parent) | |||
errx(1, "regular form violated (b)"); | |||
if (NULL == n->child) | |||
errx(1, "regular form violated (c)"); | |||
switch (n->parent->type) { | |||
case (MDOC_ROOT): | |||
/* FALLTHROUGH */ | |||
case (MDOC_HEAD): | |||
/* FALLTHROUGH */ | |||
case (MDOC_BODY): | |||
/* FALLTHROUGH */ | |||
case (MDOC_TAIL): | |||
break; | |||
default: | |||
errx(1, "regular form violated (d)"); | |||
/* NOTREACHED */ | |||
} | |||
switch (n->child->type) { | |||
case (MDOC_ROOT): | |||
/* FALLTHROUGH */ | |||
case (MDOC_ELEM): | |||
errx(1, "regular form violated (e)"); | |||
/* NOTREACHED */ | |||
default: | |||
break; | |||
} | |||
break; | |||
case (MDOC_ROOT): | |||
if (n->parent) | |||
errx(1, "regular form violated (f)"); | |||
if (NULL == n->child) | |||
errx(1, "regular form violated (10)"); | |||
switch (n->child->type) { | |||
case (MDOC_BLOCK): | |||
break; | |||
default: | |||
errx(1, "regular form violated (11)"); | |||
/* NOTREACHED */ | |||
} | |||
break; | |||
} | |||
} | } | ||