[BACK]Return to man_html.c CVS log [TXT][DIR] Up to [cvsweb.bsd.lv] / mandoc

File: [cvsweb.bsd.lv] / mandoc / man_html.c (download)

Revision 1.170, Fri Jan 18 14:36:21 2019 UTC (5 years, 2 months ago) by schwarze
Branch: MAIN
Changes since 1.169: +10 -8 lines

The .UR and .MT blocks in man(7) are represented by <a> elements
which establish phrasing context, but they can contain paragraph
breaks (which is relevant for terminal formatting, so we can't just
change the structure of the syntax tree), which are respresented
by <p> elements and cannot occur inside <a>.

Fix this by prematurely closing the <a> element in the HTML formatter.
This menas that the clickable text in HTML output is shorter than
what is represented as the link text in terminal output, but in
HTML, it is frankly impossible to have the clickable area of a
hyperlink extend across a paragraph break.  The difference in
presentation is not a major problem, and besides, paragraph breaks
inside .UR are rather poor style in the first place.

The implementation is quite tricky.  Naively closing out the <a>
prematurely would result in accessing a stale pointer when later
reaching the physical end of the .UR block.  So this commit separates
visual and structural closing of "struct tag" stack items.  Visual
closing means that the HTML element is closed but the "struct tag"
remains on the stack, to avoid later access to a stale pointer and
to avoid closing the same HTML element a second time later.

This also needs reference counting of pointers to "struct tag" stack
items because often more than one child holds a pointer to the same
parent item, and only the outermost child can safely do the physical
closing.

In the whole corpus of nearly half a million manual pages on
man.openbsd.org, this problem occurs in exactly one page: the
groff(1) version 1.20.1 manual contained in DragonFly-3.8.2, which
contains a formatting error triggering the bug.

/*	$Id: man_html.c,v 1.170 2019/01/18 14:36:21 schwarze Exp $ */
/*
 * Copyright (c) 2008-2012, 2014 Kristaps Dzonsons <kristaps@bsd.lv>
 * Copyright (c) 2013-2015, 2017-2019 Ingo Schwarze <schwarze@openbsd.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */
#include "config.h"

#include <sys/types.h>

#include <assert.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "mandoc_aux.h"
#include "mandoc.h"
#include "roff.h"
#include "man.h"
#include "out.h"
#include "html.h"
#include "main.h"

#define	MAN_ARGS	  const struct roff_meta *man, \
			  const struct roff_node *n, \
			  struct html *h

struct	man_html_act {
	int		(*pre)(MAN_ARGS);
	int		(*post)(MAN_ARGS);
};

static	void		  print_man_head(const struct roff_meta *,
				struct html *);
static	void		  print_man_nodelist(MAN_ARGS);
static	void		  print_man_node(MAN_ARGS);
static	int		  man_B_pre(MAN_ARGS);
static	int		  man_IP_pre(MAN_ARGS);
static	int		  man_I_pre(MAN_ARGS);
static	int		  man_OP_pre(MAN_ARGS);
static	int		  man_PP_pre(MAN_ARGS);
static	int		  man_RS_pre(MAN_ARGS);
static	int		  man_SH_pre(MAN_ARGS);
static	int		  man_SM_pre(MAN_ARGS);
static	int		  man_SY_pre(MAN_ARGS);
static	int		  man_UR_pre(MAN_ARGS);
static	int		  man_abort_pre(MAN_ARGS);
static	int		  man_alt_pre(MAN_ARGS);
static	int		  man_ign_pre(MAN_ARGS);
static	int		  man_in_pre(MAN_ARGS);
static	void		  man_root_post(const struct roff_meta *,
				struct html *);
static	void		  man_root_pre(const struct roff_meta *,
				struct html *);

static	const struct man_html_act man_html_acts[MAN_MAX - MAN_TH] = {
	{ NULL, NULL }, /* TH */
	{ man_SH_pre, NULL }, /* SH */
	{ man_SH_pre, NULL }, /* SS */
	{ man_IP_pre, NULL }, /* TP */
	{ man_IP_pre, NULL }, /* TQ */
	{ man_abort_pre, NULL }, /* LP */
	{ man_PP_pre, NULL }, /* PP */
	{ man_abort_pre, NULL }, /* P */
	{ man_IP_pre, NULL }, /* IP */
	{ man_PP_pre, NULL }, /* HP */
	{ man_SM_pre, NULL }, /* SM */
	{ man_SM_pre, NULL }, /* SB */
	{ man_alt_pre, NULL }, /* BI */
	{ man_alt_pre, NULL }, /* IB */
	{ man_alt_pre, NULL }, /* BR */
	{ man_alt_pre, NULL }, /* RB */
	{ NULL, NULL }, /* R */
	{ man_B_pre, NULL }, /* B */
	{ man_I_pre, NULL }, /* I */
	{ man_alt_pre, NULL }, /* IR */
	{ man_alt_pre, NULL }, /* RI */
	{ NULL, NULL }, /* RE */
	{ man_RS_pre, NULL }, /* RS */
	{ man_ign_pre, NULL }, /* DT */
	{ man_ign_pre, NULL }, /* UC */
	{ man_ign_pre, NULL }, /* PD */
	{ man_ign_pre, NULL }, /* AT */
	{ man_in_pre, NULL }, /* in */
	{ man_SY_pre, NULL }, /* SY */
	{ NULL, NULL }, /* YS */
	{ man_OP_pre, NULL }, /* OP */
	{ NULL, NULL }, /* EX */
	{ NULL, NULL }, /* EE */
	{ man_UR_pre, NULL }, /* UR */
	{ NULL, NULL }, /* UE */
	{ man_UR_pre, NULL }, /* MT */
	{ NULL, NULL }, /* ME */
};


void
html_man(void *arg, const struct roff_meta *man)
{
	struct html		*h;
	struct roff_node	*n;
	struct tag		*t;

	h = (struct html *)arg;
	n = man->first->child;

	if ((h->oflags & HTML_FRAGMENT) == 0) {
		print_gen_decls(h);
		print_otag(h, TAG_HTML, "");
		if (n != NULL && n->type == ROFFT_COMMENT)
			print_gen_comment(h, n);
		t = print_otag(h, TAG_HEAD, "");
		print_man_head(man, h);
		print_tagq(h, t);
		print_otag(h, TAG_BODY, "");
	}

	man_root_pre(man, h);
	t = print_otag(h, TAG_DIV, "c", "manual-text");
	print_man_nodelist(man, n, h);
	print_tagq(h, t);
	man_root_post(man, h);
	print_tagq(h, NULL);
}

static void
print_man_head(const struct roff_meta *man, struct html *h)
{
	char	*cp;

	print_gen_head(h);
	mandoc_asprintf(&cp, "%s(%s)", man->title, man->msec);
	print_otag(h, TAG_TITLE, "");
	print_text(h, cp);
	free(cp);
}

static void
print_man_nodelist(MAN_ARGS)
{
	while (n != NULL) {
		print_man_node(man, n, h);
		n = n->next;
	}
}

static void
print_man_node(MAN_ARGS)
{
	struct tag	*t;
	int		 child;

	if (n->type == ROFFT_COMMENT || n->flags & NODE_NOPRT)
		return;

	html_fillmode(h, n->flags & NODE_NOFILL ? ROFF_nf : ROFF_fi);

	child = 1;
	switch (n->type) {
	case ROFFT_TEXT:
		if (*n->string == '\0') {
			print_endline(h);
			return;
		}
		if (*n->string == ' ' && n->flags & NODE_LINE &&
		    (h->flags & HTML_NONEWLINE) == 0)
			print_endline(h);
		else if (n->flags & NODE_DELIMC)
			h->flags |= HTML_NOSPACE;
		t = h->tag;
		t->refcnt++;
		print_text(h, n->string);
		break;
	case ROFFT_EQN:
		t = h->tag;
		t->refcnt++;
		print_eqn(h, n->eqn);
		break;
	case ROFFT_TBL:
		/*
		 * This will take care of initialising all of the table
		 * state data for the first table, then tearing it down
		 * for the last one.
		 */
		print_tbl(h, n->span);
		return;
	default:
		/*
		 * Close out scope of font prior to opening a macro
		 * scope.
		 */
		if (HTMLFONT_NONE != h->metac) {
			h->metal = h->metac;
			h->metac = HTMLFONT_NONE;
		}

		/*
		 * Close out the current table, if it's open, and unset
		 * the "meta" table state.  This will be reopened on the
		 * next table element.
		 */
		if (h->tblt != NULL)
			print_tblclose(h);
		t = h->tag;
		t->refcnt++;
		if (n->tok < ROFF_MAX) {
			roff_html_pre(h, n);
			t->refcnt--;
			print_stagq(h, t);
			return;
		}
		assert(n->tok >= MAN_TH && n->tok < MAN_MAX);
		if (man_html_acts[n->tok - MAN_TH].pre != NULL)
			child = (*man_html_acts[n->tok - MAN_TH].pre)(man,
			    n, h);
		break;
	}

	if (child && n->child != NULL)
		print_man_nodelist(man, n->child, h);

	/* This will automatically close out any font scope. */
	t->refcnt--;
	print_stagq(h, t);

	if (n->flags & NODE_NOFILL && n->tok != MAN_YS &&
	    (n->next != NULL && n->next->flags & NODE_LINE)) {
		/* In .nf = <pre>, print even empty lines. */
		h->col++;
		print_endline(h);
	}
}

static void
man_root_pre(const struct roff_meta *man, struct html *h)
{
	struct tag	*t, *tt;
	char		*title;

	assert(man->title);
	assert(man->msec);
	mandoc_asprintf(&title, "%s(%s)", man->title, man->msec);

	t = print_otag(h, TAG_TABLE, "c", "head");
	tt = print_otag(h, TAG_TR, "");

	print_otag(h, TAG_TD, "c", "head-ltitle");
	print_text(h, title);
	print_stagq(h, tt);

	print_otag(h, TAG_TD, "c", "head-vol");
	if (man->vol != NULL)
		print_text(h, man->vol);
	print_stagq(h, tt);

	print_otag(h, TAG_TD, "c", "head-rtitle");
	print_text(h, title);
	print_tagq(h, t);
	free(title);
}

static void
man_root_post(const struct roff_meta *man, struct html *h)
{
	struct tag	*t, *tt;

	t = print_otag(h, TAG_TABLE, "c", "foot");
	tt = print_otag(h, TAG_TR, "");

	print_otag(h, TAG_TD, "c", "foot-date");
	print_text(h, man->date);
	print_stagq(h, tt);

	print_otag(h, TAG_TD, "c", "foot-os");
	if (man->os != NULL)
		print_text(h, man->os);
	print_tagq(h, t);
}

static int
man_SH_pre(MAN_ARGS)
{
	char	*id;

	switch (n->type) {
	case ROFFT_BLOCK:
		html_close_paragraph(h);
		break;
	case ROFFT_HEAD:
		id = html_make_id(n, 1);
		if (n->tok == MAN_SH)
			print_otag(h, TAG_H1, "ci", "Sh", id);
		else
			print_otag(h, TAG_H2, "ci", "Ss", id);
		if (id != NULL)
			print_otag(h, TAG_A, "chR", "permalink", id);
		break;
	case ROFFT_BODY:
		break;
	default:
		abort();
	}
	return 1;
}

static int
man_alt_pre(MAN_ARGS)
{
	const struct roff_node	*nn;
	struct tag	*t;
	int		 i;
	enum htmltag	 fp;

	for (i = 0, nn = n->child; nn != NULL; nn = nn->next, i++) {
		switch (n->tok) {
		case MAN_BI:
			fp = i % 2 ? TAG_I : TAG_B;
			break;
		case MAN_IB:
			fp = i % 2 ? TAG_B : TAG_I;
			break;
		case MAN_RI:
			fp = i % 2 ? TAG_I : TAG_MAX;
			break;
		case MAN_IR:
			fp = i % 2 ? TAG_MAX : TAG_I;
			break;
		case MAN_BR:
			fp = i % 2 ? TAG_MAX : TAG_B;
			break;
		case MAN_RB:
			fp = i % 2 ? TAG_B : TAG_MAX;
			break;
		default:
			abort();
		}

		if (i)
			h->flags |= HTML_NOSPACE;

		if (fp != TAG_MAX)
			t = print_otag(h, fp, "");

		print_text(h, nn->string);

		if (fp != TAG_MAX)
			print_tagq(h, t);
	}
	return 0;
}

static int
man_SM_pre(MAN_ARGS)
{
	print_otag(h, TAG_SMALL, "");
	if (n->tok == MAN_SB)
		print_otag(h, TAG_B, "");
	return 1;
}

static int
man_PP_pre(MAN_ARGS)
{
	switch (n->type) {
	case ROFFT_BLOCK:
		html_close_paragraph(h);
		break;
	case ROFFT_HEAD:
		return 0;
	case ROFFT_BODY:
		if (n->child != NULL &&
		    (n->child->flags & NODE_NOFILL) == 0)
			print_otag(h, TAG_P, "c",
			    n->tok == MAN_PP ? "Pp" : "Pp HP");
		break;
	default:
		abort();
	}
	return 1;
}

static int
man_IP_pre(MAN_ARGS)
{
	const struct roff_node	*nn;

	switch (n->type) {
	case ROFFT_BLOCK:
		html_close_paragraph(h);
		print_otag(h, TAG_DL, "c", "Bl-tag");
		return 1;
	case ROFFT_HEAD:
		print_otag(h, TAG_DT, "");
		break;
	case ROFFT_BODY:
		print_otag(h, TAG_DD, "");
		return 1;
	default:
		abort();
	}

	switch(n->tok) {
	case MAN_IP:  /* Only print the first header element. */
		if (n->child != NULL)
			print_man_node(man, n->child, h);
		break;
	case MAN_TP:  /* Only print next-line header elements. */
	case MAN_TQ:
		nn = n->child;
		while (nn != NULL && (NODE_LINE & nn->flags) == 0)
			nn = nn->next;
		while (nn != NULL) {
			print_man_node(man, nn, h);
			nn = nn->next;
		}
		break;
	default:
		abort();
	}
	return 0;
}

static int
man_OP_pre(MAN_ARGS)
{
	struct tag	*tt;

	print_text(h, "[");
	h->flags |= HTML_NOSPACE;
	tt = print_otag(h, TAG_SPAN, "c", "Op");

	if ((n = n->child) != NULL) {
		print_otag(h, TAG_B, "");
		print_text(h, n->string);
	}

	print_stagq(h, tt);

	if (n != NULL && n->next != NULL) {
		print_otag(h, TAG_I, "");
		print_text(h, n->next->string);
	}

	print_stagq(h, tt);
	h->flags |= HTML_NOSPACE;
	print_text(h, "]");
	return 0;
}

static int
man_B_pre(MAN_ARGS)
{
	print_otag(h, TAG_B, "");
	return 1;
}

static int
man_I_pre(MAN_ARGS)
{
	print_otag(h, TAG_I, "");
	return 1;
}

static int
man_in_pre(MAN_ARGS)
{
	print_otag(h, TAG_BR, "");
	return 0;
}

static int
man_ign_pre(MAN_ARGS)
{
	return 0;
}

static int
man_RS_pre(MAN_ARGS)
{
	switch (n->type) {
	case ROFFT_BLOCK:
		html_close_paragraph(h);
		break;
	case ROFFT_HEAD:
		return 0;
	case ROFFT_BODY:
		print_otag(h, TAG_DIV, "c", "Bd-indent");
		break;
	default:
		abort();
	}
	return 1;
}

static int
man_SY_pre(MAN_ARGS)
{
	switch (n->type) {
	case ROFFT_BLOCK:
		html_close_paragraph(h);
		print_otag(h, TAG_TABLE, "c", "Nm");
		print_otag(h, TAG_TR, "");
		break;
	case ROFFT_HEAD:
		print_otag(h, TAG_TD, "");
		print_otag(h, TAG_CODE, "c", "Nm");
		break;
	case ROFFT_BODY:
		print_otag(h, TAG_TD, "");
		break;
	default:
		abort();
	}
	return 1;
}

static int
man_UR_pre(MAN_ARGS)
{
	char *cp;

	n = n->child;
	assert(n->type == ROFFT_HEAD);
	if (n->child != NULL) {
		assert(n->child->type == ROFFT_TEXT);
		if (n->tok == MAN_MT) {
			mandoc_asprintf(&cp, "mailto:%s", n->child->string);
			print_otag(h, TAG_A, "ch", "Mt", cp);
			free(cp);
		} else
			print_otag(h, TAG_A, "ch", "Lk", n->child->string);
	}

	assert(n->next->type == ROFFT_BODY);
	if (n->next->child != NULL)
		n = n->next;

	print_man_nodelist(man, n->child, h);
	return 0;
}

static int
man_abort_pre(MAN_ARGS)
{
	abort();
}