xref: /openbsd/usr.bin/mandoc/read.c (revision 3d8817e4)
1 /*	$Id: read.c,v 1.1 2011/04/24 16:22:02 schwarze Exp $ */
2 /*
3  * Copyright (c) 2008, 2009, 2010, 2011 Kristaps Dzonsons <kristaps@bsd.lv>
4  * Copyright (c) 2010, 2011 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/stat.h>
19 #include <sys/mman.h>
20 
21 #include <assert.h>
22 #include <ctype.h>
23 #include <fcntl.h>
24 #include <stdarg.h>
25 #include <stdio.h>
26 #include <stdlib.h>
27 #include <string.h>
28 #include <unistd.h>
29 
30 #include "mandoc.h"
31 #include "libmandoc.h"
32 #include "mdoc.h"
33 #include "man.h"
34 
35 #define	REPARSE_LIMIT	1000
36 
37 struct	buf {
38 	char	 	 *buf; /* binary input buffer */
39 	size_t		  sz; /* size of binary buffer */
40 };
41 
42 struct	mparse {
43 	enum mandoclevel  file_status; /* status of current parse */
44 	enum mandoclevel  wlevel; /* ignore messages below this */
45 	int		  line; /* line number in the file */
46 	enum mparset	  inttype; /* which parser to use */
47 	struct man	 *pman; /* persistent man parser */
48 	struct mdoc	 *pmdoc; /* persistent mdoc parser */
49 	struct man	 *man; /* man parser */
50 	struct mdoc	 *mdoc; /* mdoc parser */
51 	struct roff	 *roff; /* roff parser (!NULL) */
52 	struct regset	  regs; /* roff registers */
53 	int		  reparse_count; /* finite interp. stack */
54 	mandocmsg	  mmsg; /* warning/error message handler */
55 	void		 *arg; /* argument to mmsg */
56 	const char	 *file;
57 };
58 
59 static	void	  resize_buf(struct buf *, size_t);
60 static	void	  mparse_buf_r(struct mparse *, struct buf, int);
61 static	void	  mparse_readfd_r(struct mparse *, int, const char *, int);
62 static	void	  pset(const char *, int, struct mparse *);
63 static	void	  pdesc(struct mparse *, const char *, int);
64 static	int	  read_whole_file(const char *, int, struct buf *, int *);
65 static	void	  mparse_end(struct mparse *);
66 
67 static	const enum mandocerr	mandoclimits[MANDOCLEVEL_MAX] = {
68 	MANDOCERR_OK,
69 	MANDOCERR_WARNING,
70 	MANDOCERR_WARNING,
71 	MANDOCERR_ERROR,
72 	MANDOCERR_FATAL,
73 	MANDOCERR_MAX,
74 	MANDOCERR_MAX
75 };
76 
77 static	const char * const	mandocerrs[MANDOCERR_MAX] = {
78 	"ok",
79 
80 	"generic warning",
81 
82 	/* related to the prologue */
83 	"no title in document",
84 	"document title should be all caps",
85 	"unknown manual section",
86 	"date missing, using today's date",
87 	"cannot parse date, using it verbatim",
88 	"prologue macros out of order",
89 	"duplicate prologue macro",
90 	"macro not allowed in prologue",
91 	"macro not allowed in body",
92 
93 	/* related to document structure */
94 	".so is fragile, better use ln(1)",
95 	"NAME section must come first",
96 	"bad NAME section contents",
97 	"manual name not yet set",
98 	"sections out of conventional order",
99 	"duplicate section name",
100 	"section not in conventional manual section",
101 
102 	/* related to macros and nesting */
103 	"skipping obsolete macro",
104 	"skipping paragraph macro",
105 	"skipping no-space macro",
106 	"blocks badly nested",
107 	"child violates parent syntax",
108 	"nested displays are not portable",
109 	"already in literal mode",
110 	"line scope broken",
111 
112 	/* related to missing macro arguments */
113 	"skipping empty macro",
114 	"argument count wrong",
115 	"missing display type",
116 	"list type must come first",
117 	"tag lists require a width argument",
118 	"missing font type",
119 	"skipping end of block that is not open",
120 
121 	/* related to bad macro arguments */
122 	"skipping argument",
123 	"duplicate argument",
124 	"duplicate display type",
125 	"duplicate list type",
126 	"unknown AT&T UNIX version",
127 	"bad Boolean value",
128 	"unknown font",
129 	"unknown standard specifier",
130 	"bad width argument",
131 
132 	/* related to plain text */
133 	"blank line in non-literal context",
134 	"tab in non-literal context",
135 	"end of line whitespace",
136 	"bad comment style",
137 	"unknown escape sequence",
138 	"unterminated quoted string",
139 
140 	"generic error",
141 
142 	/* related to tables */
143 	"bad table syntax",
144 	"bad table option",
145 	"bad table layout",
146 	"no table layout cells specified",
147 	"no table data cells specified",
148 	"ignore data in cell",
149 	"data block still open",
150 	"ignoring extra data cells",
151 
152 	"input stack limit exceeded, infinite loop?",
153 	"skipping bad character",
154 	"escaped character not allowed in a name",
155 	"skipping text before the first section header",
156 	"skipping unknown macro",
157 	"NOT IMPLEMENTED, please use groff: skipping request",
158 	"argument count wrong",
159 	"skipping end of block that is not open",
160 	"missing end of block",
161 	"scope open on exit",
162 	"uname(3) system call failed",
163 	"macro requires line argument(s)",
164 	"macro requires body argument(s)",
165 	"macro requires argument(s)",
166 	"missing list type",
167 	"line argument(s) will be lost",
168 	"body argument(s) will be lost",
169 
170 	"generic fatal error",
171 
172 	"not a manual",
173 	"column syntax is inconsistent",
174 	"NOT IMPLEMENTED: .Bd -file",
175 	"line scope broken, syntax violated",
176 	"argument count wrong, violates syntax",
177 	"child violates parent syntax",
178 	"argument count wrong, violates syntax",
179 	"NOT IMPLEMENTED: .so with absolute path or \"..\"",
180 	"no document body",
181 	"no document prologue",
182 	"static buffer exhausted",
183 };
184 
185 static	const char * const	mandoclevels[MANDOCLEVEL_MAX] = {
186 	"SUCCESS",
187 	"RESERVED",
188 	"WARNING",
189 	"ERROR",
190 	"FATAL",
191 	"BADARG",
192 	"SYSERR"
193 };
194 
195 static void
196 resize_buf(struct buf *buf, size_t initial)
197 {
198 
199 	buf->sz = buf->sz > initial/2 ? 2 * buf->sz : initial;
200 	buf->buf = mandoc_realloc(buf->buf, buf->sz);
201 }
202 
203 static void
204 pset(const char *buf, int pos, struct mparse *curp)
205 {
206 	int		 i;
207 
208 	/*
209 	 * Try to intuit which kind of manual parser should be used.  If
210 	 * passed in by command-line (-man, -mdoc), then use that
211 	 * explicitly.  If passed as -mandoc, then try to guess from the
212 	 * line: either skip dot-lines, use -mdoc when finding `.Dt', or
213 	 * default to -man, which is more lenient.
214 	 *
215 	 * Separate out pmdoc/pman from mdoc/man: the first persists
216 	 * through all parsers, while the latter is used per-parse.
217 	 */
218 
219 	if ('.' == buf[0] || '\'' == buf[0]) {
220 		for (i = 1; buf[i]; i++)
221 			if (' ' != buf[i] && '\t' != buf[i])
222 				break;
223 		if ('\0' == buf[i])
224 			return;
225 	}
226 
227 	switch (curp->inttype) {
228 	case (MPARSE_MDOC):
229 		if (NULL == curp->pmdoc)
230 			curp->pmdoc = mdoc_alloc(&curp->regs, curp);
231 		assert(curp->pmdoc);
232 		curp->mdoc = curp->pmdoc;
233 		return;
234 	case (MPARSE_MAN):
235 		if (NULL == curp->pman)
236 			curp->pman = man_alloc(&curp->regs, curp);
237 		assert(curp->pman);
238 		curp->man = curp->pman;
239 		return;
240 	default:
241 		break;
242 	}
243 
244 	if (pos >= 3 && 0 == memcmp(buf, ".Dd", 3))  {
245 		if (NULL == curp->pmdoc)
246 			curp->pmdoc = mdoc_alloc(&curp->regs, curp);
247 		assert(curp->pmdoc);
248 		curp->mdoc = curp->pmdoc;
249 		return;
250 	}
251 
252 	if (NULL == curp->pman)
253 		curp->pman = man_alloc(&curp->regs, curp);
254 	assert(curp->pman);
255 	curp->man = curp->pman;
256 }
257 
258 /*
259  * Main parse routine for an opened file.  This is called for each
260  * opened file and simply loops around the full input file, possibly
261  * nesting (i.e., with `so').
262  */
263 static void
264 mparse_buf_r(struct mparse *curp, struct buf blk, int start)
265 {
266 	const struct tbl_span	*span;
267 	struct buf	 ln;
268 	enum rofferr	 rr;
269 	int		 i, of, rc;
270 	int		 pos; /* byte number in the ln buffer */
271 	int		 lnn; /* line number in the real file */
272 	unsigned char	 c;
273 
274 	memset(&ln, 0, sizeof(struct buf));
275 
276 	lnn = curp->line;
277 	pos = 0;
278 
279 	for (i = 0; i < (int)blk.sz; ) {
280 		if (0 == pos && '\0' == blk.buf[i])
281 			break;
282 
283 		if (start) {
284 			curp->line = lnn;
285 			curp->reparse_count = 0;
286 		}
287 
288 		while (i < (int)blk.sz && (start || '\0' != blk.buf[i])) {
289 
290 			/*
291 			 * When finding an unescaped newline character,
292 			 * leave the character loop to process the line.
293 			 * Skip a preceding carriage return, if any.
294 			 */
295 
296 			if ('\r' == blk.buf[i] && i + 1 < (int)blk.sz &&
297 			    '\n' == blk.buf[i + 1])
298 				++i;
299 			if ('\n' == blk.buf[i]) {
300 				++i;
301 				++lnn;
302 				break;
303 			}
304 
305 			/*
306 			 * Warn about bogus characters.  If you're using
307 			 * non-ASCII encoding, you're screwing your
308 			 * readers.  Since I'd rather this not happen,
309 			 * I'll be helpful and drop these characters so
310 			 * we don't display gibberish.  Note to manual
311 			 * writers: use special characters.
312 			 */
313 
314 			c = (unsigned char) blk.buf[i];
315 
316 			if ( ! (isascii(c) &&
317 					(isgraph(c) || isblank(c)))) {
318 				mandoc_msg(MANDOCERR_BADCHAR, curp,
319 						curp->line, pos, "ignoring byte");
320 				i++;
321 				continue;
322 			}
323 
324 			/* Trailing backslash = a plain char. */
325 
326 			if ('\\' != blk.buf[i] || i + 1 == (int)blk.sz) {
327 				if (pos >= (int)ln.sz)
328 					resize_buf(&ln, 256);
329 				ln.buf[pos++] = blk.buf[i++];
330 				continue;
331 			}
332 
333 			/*
334 			 * Found escape and at least one other character.
335 			 * When it's a newline character, skip it.
336 			 * When there is a carriage return in between,
337 			 * skip that one as well.
338 			 */
339 
340 			if ('\r' == blk.buf[i + 1] && i + 2 < (int)blk.sz &&
341 			    '\n' == blk.buf[i + 2])
342 				++i;
343 			if ('\n' == blk.buf[i + 1]) {
344 				i += 2;
345 				++lnn;
346 				continue;
347 			}
348 
349 			if ('"' == blk.buf[i + 1] || '#' == blk.buf[i + 1]) {
350 				i += 2;
351 				/* Comment, skip to end of line */
352 				for (; i < (int)blk.sz; ++i) {
353 					if ('\n' == blk.buf[i]) {
354 						++i;
355 						++lnn;
356 						break;
357 					}
358 				}
359 
360 				/* Backout trailing whitespaces */
361 				for (; pos > 0; --pos) {
362 					if (ln.buf[pos - 1] != ' ')
363 						break;
364 					if (pos > 2 && ln.buf[pos - 2] == '\\')
365 						break;
366 				}
367 				break;
368 			}
369 
370 			/* Some other escape sequence, copy & cont. */
371 
372 			if (pos + 1 >= (int)ln.sz)
373 				resize_buf(&ln, 256);
374 
375 			ln.buf[pos++] = blk.buf[i++];
376 			ln.buf[pos++] = blk.buf[i++];
377 		}
378 
379  		if (pos >= (int)ln.sz)
380 			resize_buf(&ln, 256);
381 
382 		ln.buf[pos] = '\0';
383 
384 		/*
385 		 * A significant amount of complexity is contained by
386 		 * the roff preprocessor.  It's line-oriented but can be
387 		 * expressed on one line, so we need at times to
388 		 * readjust our starting point and re-run it.  The roff
389 		 * preprocessor can also readjust the buffers with new
390 		 * data, so we pass them in wholesale.
391 		 */
392 
393 		of = 0;
394 
395 rerun:
396 		rr = roff_parseln
397 			(curp->roff, curp->line,
398 			 &ln.buf, &ln.sz, of, &of);
399 
400 		switch (rr) {
401 		case (ROFF_REPARSE):
402 			if (REPARSE_LIMIT >= ++curp->reparse_count)
403 				mparse_buf_r(curp, ln, 0);
404 			else
405 				mandoc_msg(MANDOCERR_ROFFLOOP, curp,
406 					curp->line, pos, NULL);
407 			pos = 0;
408 			continue;
409 		case (ROFF_APPEND):
410 			pos = (int)strlen(ln.buf);
411 			continue;
412 		case (ROFF_RERUN):
413 			goto rerun;
414 		case (ROFF_IGN):
415 			pos = 0;
416 			continue;
417 		case (ROFF_ERR):
418 			assert(MANDOCLEVEL_FATAL <= curp->file_status);
419 			break;
420 		case (ROFF_SO):
421 			mparse_readfd_r(curp, -1, ln.buf + of, 1);
422 			if (MANDOCLEVEL_FATAL <= curp->file_status)
423 				break;
424 			pos = 0;
425 			continue;
426 		default:
427 			break;
428 		}
429 
430 		/*
431 		 * If we encounter errors in the recursive parse, make
432 		 * sure we don't continue parsing.
433 		 */
434 
435 		if (MANDOCLEVEL_FATAL <= curp->file_status)
436 			break;
437 
438 		/*
439 		 * If input parsers have not been allocated, do so now.
440 		 * We keep these instanced betwen parsers, but set them
441 		 * locally per parse routine since we can use different
442 		 * parsers with each one.
443 		 */
444 
445 		if ( ! (curp->man || curp->mdoc))
446 			pset(ln.buf + of, pos - of, curp);
447 
448 		/*
449 		 * Lastly, push down into the parsers themselves.  One
450 		 * of these will have already been set in the pset()
451 		 * routine.
452 		 * If libroff returns ROFF_TBL, then add it to the
453 		 * currently open parse.  Since we only get here if
454 		 * there does exist data (see tbl_data.c), we're
455 		 * guaranteed that something's been allocated.
456 		 * Do the same for ROFF_EQN.
457 		 */
458 
459 		rc = -1;
460 
461 		if (ROFF_TBL == rr)
462 			while (NULL != (span = roff_span(curp->roff))) {
463 				rc = curp->man ?
464 					man_addspan(curp->man, span) :
465 					mdoc_addspan(curp->mdoc, span);
466 				if (0 == rc)
467 					break;
468 			}
469 		else if (ROFF_EQN == rr)
470 			rc = curp->mdoc ?
471 				mdoc_addeqn(curp->mdoc,
472 					roff_eqn(curp->roff)) :
473 				man_addeqn(curp->man,
474 					roff_eqn(curp->roff));
475 		else if (curp->man || curp->mdoc)
476 			rc = curp->man ?
477 				man_parseln(curp->man,
478 					curp->line, ln.buf, of) :
479 				mdoc_parseln(curp->mdoc,
480 					curp->line, ln.buf, of);
481 
482 		if (0 == rc) {
483 			assert(MANDOCLEVEL_FATAL <= curp->file_status);
484 			break;
485 		}
486 
487 		/* Temporary buffers typically are not full. */
488 
489 		if (0 == start && '\0' == blk.buf[i])
490 			break;
491 
492 		/* Start the next input line. */
493 
494 		pos = 0;
495 	}
496 
497 	free(ln.buf);
498 }
499 
500 static void
501 pdesc(struct mparse *curp, const char *file, int fd)
502 {
503 	struct buf	 blk;
504 	int		 with_mmap;
505 
506 	/*
507 	 * Run for each opened file; may be called more than once for
508 	 * each full parse sequence if the opened file is nested (i.e.,
509 	 * from `so').  Simply sucks in the whole file and moves into
510 	 * the parse phase for the file.
511 	 */
512 
513 	if ( ! read_whole_file(file, fd, &blk, &with_mmap)) {
514 		curp->file_status = MANDOCLEVEL_SYSERR;
515 		return;
516 	}
517 
518 	/* Line number is per-file. */
519 
520 	curp->line = 1;
521 
522 	mparse_buf_r(curp, blk, 1);
523 
524 	if (with_mmap)
525 		munmap(blk.buf, blk.sz);
526 	else
527 		free(blk.buf);
528 }
529 
530 static int
531 read_whole_file(const char *file, int fd, struct buf *fb, int *with_mmap)
532 {
533 	struct stat	 st;
534 	size_t		 off;
535 	ssize_t		 ssz;
536 
537 	if (-1 == fstat(fd, &st)) {
538 		perror(file);
539 		return(0);
540 	}
541 
542 	/*
543 	 * If we're a regular file, try just reading in the whole entry
544 	 * via mmap().  This is faster than reading it into blocks, and
545 	 * since each file is only a few bytes to begin with, I'm not
546 	 * concerned that this is going to tank any machines.
547 	 */
548 
549 	if (S_ISREG(st.st_mode)) {
550 		if (st.st_size >= (1U << 31)) {
551 			fprintf(stderr, "%s: input too large\n", file);
552 			return(0);
553 		}
554 		*with_mmap = 1;
555 		fb->sz = (size_t)st.st_size;
556 		fb->buf = mmap(NULL, fb->sz, PROT_READ,
557 				MAP_FILE|MAP_SHARED, fd, 0);
558 		if (fb->buf != MAP_FAILED)
559 			return(1);
560 	}
561 
562 	/*
563 	 * If this isn't a regular file (like, say, stdin), then we must
564 	 * go the old way and just read things in bit by bit.
565 	 */
566 
567 	*with_mmap = 0;
568 	off = 0;
569 	fb->sz = 0;
570 	fb->buf = NULL;
571 	for (;;) {
572 		if (off == fb->sz) {
573 			if (fb->sz == (1U << 31)) {
574 				fprintf(stderr, "%s: input too large\n", file);
575 				break;
576 			}
577 			resize_buf(fb, 65536);
578 		}
579 		ssz = read(fd, fb->buf + (int)off, fb->sz - off);
580 		if (ssz == 0) {
581 			fb->sz = off;
582 			return(1);
583 		}
584 		if (ssz == -1) {
585 			perror(file);
586 			break;
587 		}
588 		off += (size_t)ssz;
589 	}
590 
591 	free(fb->buf);
592 	fb->buf = NULL;
593 	return(0);
594 }
595 
596 static void
597 mparse_end(struct mparse *curp)
598 {
599 
600 	if (MANDOCLEVEL_FATAL <= curp->file_status)
601 		return;
602 
603 	if (curp->mdoc && ! mdoc_endparse(curp->mdoc)) {
604 		assert(MANDOCLEVEL_FATAL <= curp->file_status);
605 		return;
606 	}
607 
608 	if (curp->man && ! man_endparse(curp->man)) {
609 		assert(MANDOCLEVEL_FATAL <= curp->file_status);
610 		return;
611 	}
612 
613 	if ( ! (curp->man || curp->mdoc)) {
614 		mandoc_msg(MANDOCERR_NOTMANUAL, curp, 1, 0, NULL);
615 		curp->file_status = MANDOCLEVEL_FATAL;
616 		return;
617 	}
618 
619 	roff_endparse(curp->roff);
620 }
621 
622 static void
623 mparse_readfd_r(struct mparse *curp, int fd, const char *file, int re)
624 {
625 	const char	*svfile;
626 
627 	if (-1 == fd)
628 		if (-1 == (fd = open(file, O_RDONLY, 0))) {
629 			perror(file);
630 			curp->file_status = MANDOCLEVEL_SYSERR;
631 			return;
632 		}
633 
634 	svfile = curp->file;
635 	curp->file = file;
636 
637 	pdesc(curp, file, fd);
638 
639 	if (0 == re && MANDOCLEVEL_FATAL > curp->file_status)
640 		mparse_end(curp);
641 
642 	if (STDIN_FILENO != fd && -1 == close(fd))
643 		perror(file);
644 
645 	curp->file = svfile;
646 }
647 
648 enum mandoclevel
649 mparse_readfd(struct mparse *curp, int fd, const char *file)
650 {
651 
652 	mparse_readfd_r(curp, fd, file, 0);
653 	return(curp->file_status);
654 }
655 
656 struct mparse *
657 mparse_alloc(enum mparset inttype, enum mandoclevel wlevel, mandocmsg mmsg, void *arg)
658 {
659 	struct mparse	*curp;
660 
661 	assert(wlevel <= MANDOCLEVEL_FATAL);
662 
663 	curp = mandoc_calloc(1, sizeof(struct mparse));
664 
665 	curp->wlevel = wlevel;
666 	curp->mmsg = mmsg;
667 	curp->arg = arg;
668 	curp->inttype = inttype;
669 
670 	curp->roff = roff_alloc(&curp->regs, curp);
671 	return(curp);
672 }
673 
674 void
675 mparse_reset(struct mparse *curp)
676 {
677 
678 	memset(&curp->regs, 0, sizeof(struct regset));
679 
680 	roff_reset(curp->roff);
681 
682 	if (curp->mdoc)
683 		mdoc_reset(curp->mdoc);
684 	if (curp->man)
685 		man_reset(curp->man);
686 
687 	curp->file_status = MANDOCLEVEL_OK;
688 	curp->mdoc = NULL;
689 	curp->man = NULL;
690 }
691 
692 void
693 mparse_free(struct mparse *curp)
694 {
695 
696 	if (curp->pmdoc)
697 		mdoc_free(curp->pmdoc);
698 	if (curp->pman)
699 		man_free(curp->pman);
700 	if (curp->roff)
701 		roff_free(curp->roff);
702 
703 	free(curp);
704 }
705 
706 void
707 mparse_result(struct mparse *curp, struct mdoc **mdoc, struct man **man)
708 {
709 
710 	if (mdoc)
711 		*mdoc = curp->mdoc;
712 	if (man)
713 		*man = curp->man;
714 }
715 
716 void
717 mandoc_vmsg(enum mandocerr t, struct mparse *m,
718 		int ln, int pos, const char *fmt, ...)
719 {
720 	char		 buf[256];
721 	va_list		 ap;
722 
723 	va_start(ap, fmt);
724 	vsnprintf(buf, sizeof(buf) - 1, fmt, ap);
725 	va_end(ap);
726 
727 	mandoc_msg(t, m, ln, pos, buf);
728 }
729 
730 void
731 mandoc_msg(enum mandocerr er, struct mparse *m,
732 		int ln, int col, const char *msg)
733 {
734 	enum mandoclevel level;
735 
736 	level = MANDOCLEVEL_FATAL;
737 	while (er < mandoclimits[level])
738 		level--;
739 
740 	if (level < m->wlevel)
741 		return;
742 
743 	if (m->mmsg)
744 		(*m->mmsg)(er, level, m->file, ln, col, msg);
745 
746 	if (m->file_status < level)
747 		m->file_status = level;
748 }
749 
750 const char *
751 mparse_strerror(enum mandocerr er)
752 {
753 
754 	return(mandocerrs[er]);
755 }
756 
757 const char *
758 mparse_strlevel(enum mandoclevel lvl)
759 {
760 	return(mandoclevels[lvl]);
761 }
762