/* $Id: manpage.c,v 2.0.1.57 2000/02/24 23:58:20 greyham Exp greyham $
 * stuff to do with manual page outputing
 */

#include "c2man.h"

#include <errno.h>
#include <ctype.h>

#include "manpage.h"
#include "strconcat.h"
#include "strappend.h"
#include "semantic.h"
#include "output.h"

#ifdef I_SYS_FILE
#include <sys/file.h>
#endif

/* list of manual pages */
ManualPage *firstpage = NULL;
ManualPage **lastpagenext = &firstpage;

void dummy() {}

void
new_manual_page(comment, decl_spec, declarator)
     char *comment;
     DeclSpec *decl_spec;
     Declarator *declarator;
{
    ManualPage *newpage;

    /* check that we really want a man page for this */
    if ((!comment) ||
	!inbasefile ||
	(!variables_out && !is_function_declarator(declarator)) ||
	(decl_spec->flags & DS_JUNK) ||
	(!static_out && (decl_spec->flags & DS_STATIC) && !header_file) ||

	/* only document extern stuff if it's in a header file, or includes a
	 * function definition.
	 */
	((decl_spec->flags & DS_EXTERN) && !header_file &&
				    declarator->type != DECL_FUNCDEF))
    {
	free_decl_spec(decl_spec);
	free_declarator(declarator);
	safe_free(comment);
	return;
    }

    declarator->comment = comment;

    newpage = (ManualPage *)safe_malloc(sizeof *newpage);
    newpage->decl_spec = (DeclSpec *)safe_malloc(sizeof *newpage->decl_spec);
    newpage->declarator = declarator;

    *newpage->decl_spec = *decl_spec;
    newpage->sourcefile = strduplicate(basefile);
    newpage->sourcetime = basetime;

    *lastpagenext = newpage;
    newpage->next = NULL;
    lastpagenext = &newpage->next;
}

void free_manual_page(page)
     ManualPage *page;
{
    free_decl_spec(page->decl_spec);
    free(page->decl_spec);
    free_declarator(page->declarator);
    safe_free(page->sourcefile);
}

/* free the list of manual pages */
void free_manual_pages(first)
     ManualPage *first;
{
    ManualPage *page, *next;

    /* free any terse description read from the file */
    if (group_terse && !terse_specified)
    {
    	free(group_terse);
	group_terse = NULL;
    }

    for (page = first;page;page = next)
    {
	next = page->next;
	free_manual_page(page);
	free(page);
    }
}

/* allocate a substring starting at start, ending at end (NOT including *end) */
char *alloc_string(start, end)
     const char *start;
     const char *end;
{
    int len = end - start;
    char *ret;
    if (len == 0)	return NULL;

    ret = (char *)safe_malloc((size_t)len+1);

    strncpy(ret,start,len);
    ret[len] = '\0';

    return ret;
}

/* remember the terse description from the first comment in a file */
void remember_terse(comment)
     char *comment;
{
    char *c, *d;

    enum { STUFF, LEADSPACE, DASH, TRAILSPACE, FOUND } state = STUFF;

    /* if we've found a terse comment in a previous file, or one was
     * specified on the command line, forget it.
     */
    if (group_terse)	return;

    /* look for a whitespace surrounded sequence of dashes to skip */
    for (c = comment;*c && state != FOUND;c++)
    {
	switch (state)
	{
	case STUFF:	if (isspace(*c))	state = LEADSPACE;
			break;
	case LEADSPACE:	if (*c == '-')		state = DASH;
			else if (!isspace(*c))	state = STUFF;
			break;
	case DASH:	if (isspace(*c))	state = TRAILSPACE;
			else if (*c != '-')	state = STUFF;
			break;
	case TRAILSPACE:if (!isspace(*c))	{ c--; state = FOUND; }
			break;
	case FOUND:	break;
	}
    }

    /* if no dashes were found, go back to the start */
    if (state != FOUND)	c = comment;

    d = c + 1;

    while (*d && *d != '\n')
	d++;

    group_terse = alloc_string(c,d);
}

/* output a comment in man page form, followed by a newline */
void
output_comment(comment)
const char *comment;
{
    if (!comment || !comment[0])
	output->text("Not Documented.");
    else if (fixup_comments)
	output->description(comment);
    else
	output->text(comment);

    output->character('\n');
}

/* output the phrase "a[n] <type name>" */
void output_conjunction(text)
char *text;
{
    output->character('a');
    if (strchr("aAeEiIoOuU",text[0]))	output->character('n');
    output->character(' ');
    output->code(text);
}

/* output the description for an identifier; be it return value or param */
static void output_identifier_description(comment, outfunc,
						    decl_spec, declarator)
    const char *comment;		/* comment for this identifier */
    void (*outfunc) _((const char *));	/* function to output comment */
    const DeclSpec *decl_spec;
    const Declarator *declarator;
{
    /* one day, this may document the contents of structures too */

    /* output list of possible enum values, if any */
    if (decl_spec->enum_list)
    {
	int maxtaglen = 0;
	char *longestag = NULL;
	int descriptions = 0;
	int entries = 0;
	Enumerator *e;
	int is_first = 1;
	boolean started = FALSE;

	/* don't output the "Not Doc." message for enums */
	if (comment)
	{
	    (*outfunc)(comment);
	    output->blank_line();
	}

	/* see if any have descriptions */
	for (e = decl_spec->enum_list->first; e; e = e->next)
	    if (e->name[0] != '_')
	    {
		int taglen = strlen(e->name);
		if (taglen > maxtaglen)
		{
		    maxtaglen = taglen;
		    longestag = e->name;
		}
		if (e->comment)	descriptions = 1;
		entries++;
	    }

	/* if there are a lot of them, the list may be automatically generated,
	 * and probably isn't wanted in every manual page.
	 */
	if (entries > 20)
	{
	    char entries_s[15];
	    sprintf(entries_s, "%d", entries);
	    output->text("Since there are ");
	    output->text(entries_s);
	    output->text(" possible values for ");
	    output_conjunction(decl_spec->text);
	    output->text(", they are not all listed here.\n");
	}
	else if (entries > 0)   /* skip the pathological case */
	{
	    /* the number of possibilities is reasonable; list them all */
	    output->text("Possible values for ");
	    output_conjunction(decl_spec->text);
	    output->text(" are as follows:\n");

	    for (e = decl_spec->enum_list->first; e; e = e->next)
	    {
		/* don't print names with a leading underscore! */
		if (e->name[0] == '_')	continue;

		if (e->group_comment)
		{
		    /* break out of table mode for the group comment */
		    if (started)
		    {
			if (descriptions)
			    output->table_end();
			else
			    output->list_end();
			started = FALSE;
		    }
		    output->indent();
		    output_comment(e->group_comment);
		}

		if (!started)
		{
		    if (descriptions)
			output->table_start(longestag);
		    else
			output->list_start();
		    started = TRUE;
		}

		if (descriptions)
		    output->table_entry(e->name, e->comment);
		else
		{
		    if (!is_first)
			output->list_separator();
		    is_first = 0;
		    output->list_entry(e->name);
		}
	    }

	    if (started)
	    {
		if (descriptions)
		    output->table_end();
		else
		    output->list_end();
	    }
	}
    }
    else
	(*outfunc)(comment);
}

/* is there automatic documentation here? */
static boolean auto_documented(page)
const ManualPage *page;
{
    /* one day we may handle structs too */
    return
	page->decl_spec->enum_list != NULL;    /* enums are self-documenting. */
}

/* decide if a manual page needs a RETURNS section.
 * If this is true, then output_identifier_description must be able to generate
 * sensible output for it.
 */
static boolean needs_returns_section(page)
const ManualPage *page;
{
    return
	(page->returns && page->returns[0]) ||
	(auto_documented(page) && is_function_declarator(page->declarator));
}

/* does this declarator have documented parameters? */
boolean has_documented_parameters(d)
const Declarator *d;
{
    if (has_parameters(d))
    {
	Parameter *p;

	for (p = d->head->params.first; p != NULL; p = p->next)
	    if (p->declarator->comment || always_document_params)
		return TRUE;
    }
    return FALSE;
}

/* Output the list of function parameter descriptions.
 */
void
output_parameter_descriptions (params, function)
ParameterList *params;
char *function;
{
    Parameter *p;
    boolean tag_list_started = FALSE;

    for (p = params->first; p != NULL; p = p->next)
    {
	if (p->suppress ||
	    (!always_document_params && p->declarator->comment == NULL))
		continue;

	if (!tag_list_started)
	{
	  output->tag_list_start();
	  tag_list_started = TRUE;
	}

	if (p->duplicate)
	    output->tag_entry_start_extra();
	else
	    output->tag_entry_start();

	output_parameter(p);

	/* include function name if it's a duplicate */
	if (p->duplicate)
	    output->tag_entry_end_extra(function);
	else
	    output->tag_entry_end();

	output_identifier_description(p->declarator->comment, output_comment,
						&p->decl_spec, p->declarator);
    }

    if (tag_list_started)
	output->tag_list_end();
}

/* split out the 'Returns:' section of a function comment */
boolean
split_returns_comment(comment, description, returns)
     char *comment;
     char **description;
     char **returns;
{
    char *retstart;

    for (retstart = comment;
	 retstart;
	 retstart = strchr(retstart,'\n'))
    {
	if (*retstart == '\n')	retstart++;	/* skip the newline */

	if (!strncmpi(retstart, "returns",(size_t)7))
	{
	    char *descend = retstart - 2;	/* back before newline */

	    /* go back to the end of the description in case there were
	     * linefeeds before the returns.
	     */
	    while (descend > comment && isspace(*descend))
		descend--;

	    *description =
		descend > comment ? alloc_string(comment,descend+1) : NULL;

	    retstart += 7;

	    while (*retstart == ':' || isspace(*retstart))
		retstart++;

	    if (*retstart)
		*returns = strduplicate(retstart);
	    else
	    	*returns = NULL;
	    return TRUE;
	}
    }

    *description = comment;
    *returns = NULL;
    return FALSE;
}

/* skip to past the dash on the first line, if there is one
 * The dash must be surrounded by whitespace, so hyphens are not skipped.
 */
const char *skipdash(c)
const char *c;
{
    const char *d;

    /* ignore anything on the first line, up to a dash (if any) */
    for (d = c + 1; *d && *d != '\n' && *d != '-'; d++)
	;

    if (isspace(d[-1]) && d[0] == '-' && isspace(d[1]))
    {
	do
	    d++;
	while (*d && *d != '\n' && isspace(*d));

	if (*d && *d != '\n')	c = d;
    }
    return c;
}

/* split the function comment into manual page format.
 * returns TRUE if the DESCRIPTION field was explicit.
 */
boolean
split_function_comment(comment, identifier_name,
				    terse, description, returns, extra_sections)
    const char *comment;
    const char *identifier_name;
     char **terse;
     char **description;
     char **returns;
     Section **extra_sections;
{
    const char *c, *start_text = NULL, *end_text = NULL;
    char **put_ptr = NULL;
    Section *first_section, **lastnextsection = &first_section;
    boolean explicit_description = FALSE;
    boolean lastblank = TRUE;
    boolean skip_dash = FALSE;

    *description = *returns = NULL;
    if (terse)	*terse = NULL;

    /* for each line... */
    for (c = comment; *c;)
    {
	const char *start_line = c;
	boolean section_heading;
	/* remember if it's a blank line */
	if (*c == '\n')
	{
	    lastblank = TRUE;
	    c++;
	    continue;
	}

	/* if the last one was blank, perhaps this one is a section heading
	 */
	if (lastblank)
	{
	    boolean need_colon = FALSE;

	    /* see if we've found the start of a SECTION */
	    while (isalnum(*c) || *c == ' ' || *c == '/')
	    {
		if (isspace(*c)) need_colon = TRUE;
		c++;
	    }

	    section_heading = (!need_colon && *c == '\n') ||
			(*c == ':' && (!need_colon || *(c+1) == '\n')) ||
			(!need_colon && *c == '\0' && start_line == comment);
	}
	else
	    section_heading = FALSE;

	lastblank = FALSE;	/* this one's not blank; for next time */

	if (section_heading)
	{
	    size_t section_len = c - start_line; /* length of section name */

	    /* yes, we've found a SECTION; store the previous one (if any) */
	    if (put_ptr && start_text)
	    {
		if (skip_dash)	start_text = skipdash(start_text);
		*put_ptr = alloc_string(start_text,end_text);
	    }

	    skip_dash = FALSE;

	    /* check for comments that start with the name of the identifier */
	    if (start_line == comment &&
		!strncmp(start_line, identifier_name, section_len))
	    {
		put_ptr = description;
	    }

	    /* only accept NAME if not grouped */
	    else if (terse &&
		     (!strncmpi(start_line,"NAME", section_len) ||
		      !strncmpi(start_line,"FUNCTION", section_len) ||
		      !strncmpi(start_line,"PROCEDURE", section_len) ||
		      !strncmpi(start_line,"ROUTINE", section_len))
		     )

	    {
		put_ptr = terse;
		skip_dash = TRUE;
	    }
	    else if (!strncmpi(start_line,"DESCRIPTION", section_len))
	    {
		explicit_description = TRUE;
		put_ptr = description;
	    }
	    else if (!strncmpi(start_line,"RETURNS", section_len))
	    {
		put_ptr = returns;
	    }
	    else
	    {
		/* allocate a new section */
		Section *new_section =
				(Section *)safe_malloc(sizeof *new_section);

		*lastnextsection = new_section;
		lastnextsection = &new_section->next;

		new_section->name = alloc_string(start_line,c);
		strtoupper(new_section->name);
		new_section->text = NULL;
		new_section->been_output = FALSE; /* not been output yet */
		put_ptr = &new_section->text;
	    }

	    /* defer decision about where text starts till we find some */
	    start_text = NULL;

	    if (*c == ':')	/* skip the terminating : */
	    {
		c++;

		/* skip forward to the start of the text */
		while (*c && *c != '\n' && isspace(*c))
		    c++;

		/* if we find the text here, then we've got it */
		if (*c && *c != '\n')
		    start_text = c;
	    }
	}
	else
	{
	    /* are we looking at the top of the function comment? */
	    if (start_line == comment)
	    {
		/* only look for terse comment if not grouped together */
		if (terse)
		{
		    const char *endterse, *afterdash = skipdash(start_line);

		    /* find the end of the terse comment */
		    while (*c && *c != '\n')
		    {
			c++;
		      /* '.' ends terse description only if it ends sentence */
			if (*(c-1)=='.' && *c && isspace(*c)) 
			  break;
		    }

		    endterse = c;
		    *terse = alloc_string(
			afterdash < endterse ? afterdash : start_line,
			endterse);

		    /* skip it if it's a ., and any trailing spaces */
		    if (*c == '.')
			do c++; while (*c && *c != '\n' && isspace(*c));

		    start_text = NULL;	/* look for it */

		    if (*c && *c != '\n')
			/* actually, it's a description, starting here */
			start_text = c;
		}
		/* must be a description starting at the beginning of the line.
		 */
		else
		    start_text = start_line;

		put_ptr = description;
	    }
	    else
		/* have we just located the first real text in a section? */
		if (put_ptr && !start_text)	start_text = start_line;
	}

	/* skip the line */
	if (*c && *c != '\n')
	    while (*c && *c != '\n')	c++;

	end_text = c;	/* so far, the text ends at the end of this line */
	if (*c)	c++;
    }

    /* store the last one */
    if (put_ptr && start_text)
    {
	if (skip_dash)	start_text = skipdash(start_text);
	*put_ptr = alloc_string(start_text,end_text);
    }

    /* terminate (or nuke) section list */
    *lastnextsection = NULL;

    *extra_sections = first_section;

    return explicit_description;
}

/* see if two parameters are declared identically */
boolean params_identical(first, second)
     Parameter *first;
     Parameter *second;
{
    return
	first->decl_spec.flags == second->decl_spec.flags &&

	/* there may be no decl_spec.text if it's an ellipsis arg */
	((!first->decl_spec.text && !second->decl_spec.text) ||
	 (first->decl_spec.text && second->decl_spec.text &&
	  !strcmp(first->decl_spec.text, second->decl_spec.text))) &&

	((!first->declarator->text && !second->declarator->text) ||
	 (first->declarator->text && second->declarator->text &&
	  !strcmp(first->declarator->text, second->declarator->text)));
}

/* search all the parameters in this grouped manual page for redundancies */
boolean mark_duplicate_parameters(firstpage)
     ManualPage *firstpage;
{
    Parameter *param;
    boolean any = FALSE;
    ManualPage *page;

    for (page = firstpage; page; page = page->next)
    {
	if (has_parameters(page->declarator))
	for (param = page->declarator->head->params.first; param;
							param = param->next)
	{
	    ManualPage *otherpage;
	    Parameter *otherparam;

	    if (always_document_params || param->declarator->comment)
		any = TRUE;

	    for (otherpage = page->next; otherpage;
						otherpage = otherpage->next)
	    {
		if (has_parameters(otherpage->declarator))
		for (otherparam = otherpage->declarator->head->params.first;
		     otherparam;
		     otherparam = otherparam->next)
		{
		    /* do these two look the same? */
		    if (params_identical(param, otherparam))
		    {
			/* order is important for bit positions */
			enum { NEITHER, US, THEM, BOTH } has_comm = NEITHER;

			/* work out who has the comment */
			if (param->declarator->comment)	has_comm |= US;
			if (otherparam->declarator->comment) has_comm |= THEM;

			switch(has_comm)
			{
			case NEITHER:
			case US:
			    otherparam->suppress = TRUE;
			    break;
			case THEM:
			    param->suppress = TRUE;
			    break;
			case BOTH:
			    if (!strcmp(param->declarator->comment,
					    otherparam->declarator->comment))
				otherparam->suppress = TRUE;
			    else
			    {
				param->duplicate = TRUE;
				otherparam->duplicate = TRUE;
			    }
			    break;
			}
		    }
		}
	    }
	}
    }
    return any;
}

/* output a formatting string so that it works with filling on */
void output_format_string(fmt)
const char *fmt;
{
    while (*fmt)
    {
	output->character(*fmt);

	if (*fmt++ == '\n')
	    output->break_line();	/* break the line */
    }
}

/* write the warning for the header */
void output_warning()
{
    output->comment();
    output->text("WARNING! THIS FILE WAS GENERATED AUTOMATICALLY BY ");
    output->text(progname);
    output->text("!\n");
    output->comment();
    output->text("DO NOT EDIT! CHANGES MADE TO THIS FILE WILL BE LOST!\n");
}

void output_includes()
{
    IncludeFile *incfile;

    for (incfile = first_include; incfile; incfile=incfile->next)
    {
	char *name = incfile->name;
	boolean surrounded = *name == '"' || *name == '<';

	output->text("#include ");
	if (!surrounded)	output->character('<');
	output->text(name);
	if (!surrounded)	output->character('>');
	output->text("\n");
	output->break_line();
	}
}

int exclude_section(section)
const char *section;
{
    ExcludeSection *exclude;

    for (exclude = first_excluded_section ; exclude ; exclude = exclude->next)
	if (!strcmp(section, exclude->name)) return 1;

    return 0;
}


/* Writes the entire contents of the manual page specified by basepage. */
void
output_manpage(firstpage, basepage, input_files, title, section)
    /* the first page in the list of all manual pages.  This is used to build
     * the SEE ALSO section of related pages when group_together is false.
     */
    ManualPage *firstpage;

    /* the base page from which the output manual page will be generated.  if
     * group_together indicates that the user wanted grouped pages, basepage
     * will always be the same as firstpage, and all the ManualPage's in the
     * list will be grouped together into the one output page.
     */
    ManualPage *basepage;

    int input_files;
    const char *title;
    const char *section;
{
    ManualPage *page;
    boolean need_returns;
    char *terseout, *terse = NULL;
    boolean exclude_description = exclude_section("DESCRIPTION");

    /* check if there's more than one page in the group */
    boolean grouped = group_together && firstpage->next;

    /* split up all the function comments for this page */
    for (page = basepage; page; page = page->next)
    {
	boolean explicit_description =
	    split_function_comment(page->declarator->comment,
		page->declarator->name,
		group_together ? (char **)NULL : &terse,
		&page->description,&page->returns,&page->first_section);

	/* we may need to look harder if RETURNS wasn't easy to find in the
	 * function comment.
	 */
	if (page->returns == NULL)
	{
	    /* if there was a retcomment supplied by the declarator, use it if
	     * we couldn't split anything from the function comment.
	     */
	    if (page->declarator->retcomment)
	    {
		page->returns = page->declarator->retcomment;

		/* page->returns now owns the string */
		page->declarator->retcomment = NULL;
	    }
	    else
		/* if there wasn't a RETURNS section, and the DESCRIPTION field
		 * was not explicit, see if we can split one out of the
		 * description field.
		 */
		if (!explicit_description)
		{
		    char *newdesc;
		    if (split_returns_comment(page->description, &newdesc,
							    &page->returns))
		    {
			free(page->description);
			page->description = newdesc;
		    }
		}
	}

	if (!group_together)	break;
    }

    /* work out what we'll actually print as a terse description */
    terseout = group_terse ? group_terse : (terse ? terse : "Not Described");

    output->header(basepage, input_files, grouped,
		title ? title : basepage->declarator->name, terseout, section);

    output->name(NULL);
    /* output the names of all the stuff documented on this page */
    for (page = basepage; page; page = page->next)
    {
	output->name(page->declarator->name);

	if (!group_together)	break;

	if (page->next)	output->text(",\n");
    }

    output->terse_sep();
    output->text(terseout);
    output->character('\n');

    if (!exclude_section("SYNOPSIS"))
    {
	output->section("SYNOPSIS");

	output->code_start();

	/* list the include files the user asked us to */
	output_includes();

	/* if it's a header file, say to #include it */
	if (header_file)
	{
	    output->text("#include <");
	    if (header_prefix)
	    {
		output->text(header_prefix);
		output->character('/');
	    }
	    output->text(basefile);
	    output->text(">\n");
	}

	/* can't just use .PP; that may reset our font */
	if (first_include || header_file)	output->blank_line();

	for (page = basepage; page; page = page->next)
	{
	    output_format_string(decl_spec_prefix);

	    /* make sure variables are prefixed extern */
	    if (!(page->decl_spec->flags & DS_STATIC) &&
		!is_function_declarator(page->declarator) &&
		!strstr(page->decl_spec->text, "extern"))
		output->text("extern ");

	    output_decl_spec(page->decl_spec);
	    output_format_string(declarator_prefix);

	    /* format it nicely if there's more than one parameter */
	    output_declarator(page->declarator,
		page->declarator->head->params.first !=
		page->declarator->head->params.last);

	    output->text(";\n");

	    if (!grouped)	break;
	    if (page->next)	output->blank_line();
	}

	output->code_end();
    }

    /* only output paramaters if there actually are some,
     * not including merely (void)
     */
    if (!exclude_section("PARAMETERS") &&
	    ((grouped && mark_duplicate_parameters(basepage)) ||
	     (!grouped && has_documented_parameters(basepage->declarator))))
    {
	output->section("PARAMETERS");

	for (page = basepage; page; page = page->next)
	{
	    if (has_parameters(page->declarator))
		output_parameter_descriptions(&page->declarator->head->params,
						    page->declarator->name);
	    if (!grouped)	break;	/* only do first page */
	}
    }

    if (!exclude_description)
	output->section("DESCRIPTION");

    if (grouped)
    {
	need_returns = FALSE;
	for (page = basepage; page; page = page->next)
	{
	    if (needs_returns_section(page))	need_returns = TRUE;

	    if (!exclude_description)
	    {
		/* enum variables are documented in DESCRIPTION */
		if (auto_documented(page) &&
				    !is_function_declarator(page->declarator))
		{
		    output->sub_section(page->declarator->name);
		    output_identifier_description(page->description,
			output_comment, page->decl_spec, page->declarator);
		}
		else if (page->description)
		{
		    output->sub_section(page->declarator->name);
		    output_comment(page->description);
		}
	    }

	    safe_free(page->description);
	}
    }
    else
    {
	need_returns = needs_returns_section(basepage);

	if (!exclude_description)
	{
	    const char *descr = basepage->description
					? basepage->description : terseout;

	    if (auto_documented(page) &&
				    !is_function_declarator(page->declarator))
		output_identifier_description(descr, output_comment,
					    page->decl_spec, page->declarator);
	    else
		output_comment(descr);
	}

	safe_free(basepage->description);
    }

    /* terse can now never be a static string */
    safe_free(terse);

    if (need_returns && !exclude_section("RETURNS"))
    {
	output->section("RETURNS");

	for (page = basepage; page; page = page->next)
	{
	    if (needs_returns_section(page))
	    {
		if (grouped) output->sub_section(page->declarator->name);

		output_identifier_description(page->returns, output->returns,
					    page->decl_spec, page->declarator);
		safe_free(page->returns);
	    }

	    if (!grouped)	break;
	}
    }

    /* output any other sections */
    for (page = basepage; page; page = page->next)
    {
	Section *section, *next;

	for (section = page->first_section; section; section = next)
	{
	    next = section->next;

	    if (!section->been_output && section->text &&
		strncmpi(section->text,"none",4) &&
		 !exclude_section(section->name))
	    {
		output->section(section->name);
		if (grouped) output->sub_section(page->declarator->name);
		output_comment(section->text);
		section->been_output = TRUE;

		if (grouped && page->next)
		{
		    ManualPage *other_page = page->next;

		    /* look through all the other pages for matching sections */
		    for (; other_page; other_page = other_page->next)
		    {
			Section *other_section = other_page->first_section;
			for (;other_section; other_section =
							    other_section->next)
			{
			    if (other_section->been_output ||
				strcmp(other_section->name, section->name))
				continue;

			    output->sub_section(other_page->declarator->name);
			    output_comment(other_section->text);
			    other_section->been_output = TRUE;
			}
		    }
		}
	    }


	    /* free this section */
	    free(section->name);
	    safe_free(section->text);
	    free(section);
	}

	if (!grouped)	break;
    }

    /* only output SEE ALSO if not grouped */
    if (!group_together)
    {
	ManualPage *also;

	/* add the SEE ALSO section */
	/* look for any other functions to refer to */
	for (also = firstpage; also && also == basepage; also = also->next)
		;

	if (also && !exclude_section("SEE ALSO"))	/* did we find at least one? */
	{
	    int isfirst = 1;

	    output->section("SEE ALSO");

	    for (also = firstpage; also; also = also->next)
	    {
		if (also == basepage)	continue;

		if (!isfirst)
		    output->text(",\n");
		else
		    isfirst = 0;

		output->reference(also->declarator->name);
	    }

	    output->character('\n');
	}
    }

    if (!make_embeddable)
	output->file_end();
}


/* generate output filename based on a string */
char *page_file_name(based_on, object_type, extension)
    /* string to base the name on; this will be the name of an identifier or
     * the base of the input file name.
     */
    const char *based_on;
    enum Output_Object object_type;	/* class of object documented */
    const char *extension;		/* file extension to use */
{
    char *filename;
    const char *subdir = output_object[object_type].subdir;

#ifndef FLEXFILENAMES
    char *basename;
    int chopoff = 14 - strlen(extension) - 1;

    basename = strduplicate(based_on);
    if (strlen(basename) > chopoff)
	basename[chopoff] = '\0';
#else
    const char *basename = based_on;
#endif

    filename = strduplicate(output_dir);

    if (subdir)
    {
	if (filename)	filename = strappend(filename, "/", NULLCP);
	filename = strappend(filename, subdir, NULLCP);
    }

    if (filename)	filename = strappend(filename, "/", NULLCP);
    filename = strappend(filename, basename,".",extension, NULLCP);

#ifndef FLEXFILENAMES
    free(basename);
#endif
    return filename;
}

/* determine the output page type from a declaration */
enum Output_Object page_output_type(decl_spec, declarator)
const DeclSpec *decl_spec;
const Declarator *declarator;
{
    boolean is_static = decl_spec->flags & DS_STATIC;
    return is_function_declarator(declarator)
	? (is_static ? OBJECT_STATIC_FUNCTION : OBJECT_FUNCTION)
	: (is_static ? OBJECT_STATIC_VARIABLE : OBJECT_VARIABLE);
}

/* determine the extension/section from an output type */
const char *page_manual_section(output_type)
enum Output_Object output_type;
{
    return	output_object[output_type].extension ?
		output_object[output_type].extension : manual_section;
}

/* remove an existing file, if it exists & we have write permission to it */
int remove_old_file(name)
const char *name;
{
#ifdef HAS_ACCESS
    /* check that we have write premission before blasting it */
    if (access(name,W_OK) == -1)
    {
	if (errno != ENOENT)
	{
	    my_perror("can't access output file", name);
	    return FALSE;
	}
     }
    else
#endif
    {
	/* if it exists, blast it */
	if (unlink(name) == -1 && errno != ENOENT)
	{
	    my_perror("error unlinking old link file", name);
	    return FALSE;
	}
    }
    return TRUE;
}

/* output all the manual pages in a list */
void output_manual_pages(first, input_files, link_type)
    ManualPage *first;
    int input_files;	/* number of different input files */
    enum LinkType link_type;	/* how grouped pages will be linked */
{
    ManualPage *page;
    int tostdout = output_dir && !strcmp(output_dir,"-");

    char *filename = NULL;

    /* output each page, in turn */
    for (page = first; page; page = page->next)
    {
	char *input_file_base = NULL;
	enum Output_Object output_type =
			page_output_type(page->decl_spec, page->declarator);

	/* the manual name is used as the output file extension, and also in
	 * the nroff output header.
	 */
	const char *section = page_manual_section(output_type);

	/* work out the base name of the file this was generated from */
	if (page->sourcefile)
	{
	    const char *base = strrchr(firstpage->sourcefile, '/');
	    const char *last;

	    /* use the file name as the manual page title */
	    if (base == NULL)
		base = firstpage->sourcefile;
	    else
		base++;
	    last = strrchr(base, '.');
	    if (last == NULL)
		last = base + strlen(base);

	    input_file_base = alloc_string(base, last);
	}

	if (!tostdout)
	{
	    safe_free(filename);	/* free previous, if any */
	    filename = page_file_name(
		use_input_name && input_file_base
				? input_file_base : page->declarator->name,
		output_type, section);
	    fprintf(stderr,"generating: %s\n",filename);

	    /* a previous run may have left links, so nuke old file first */
	    if (!remove_old_file(filename))	exit(1);

	    if (freopen(filename, "w", stdout) == NULL)
	    {
		my_perror("error opening output file", filename);
		free(filename);
		exit(1);
	    }
	}

	/* do the page itself */
	output_manpage(first, page, input_files,
	    group_together && input_file_base ? input_file_base
					      : page->declarator->name,
	    group_together ? manual_section : section);

	safe_free(input_file_base);

	/* don't continue if grouped, because all info went into this page */
	if (group_together)		break;

	if (tostdout &&	page->next)	output->character('\f');
    }

    /* close the last output file if there was one */
    if (!tostdout && fclose(stdout) == EOF)
    {
	my_perror("error linking closing file", filename);
	exit(1);
    }

    /* if pages are grouped, just link the rest to the first */
    if (group_together && !tostdout && link_type != LINK_NONE)
    {
	for (page=use_input_name && first->sourcefile ? first : first->next;
						    page; page = page->next)
	{
	    enum Output_Object output_type =
			page_output_type(page->decl_spec, page->declarator);
	    const char *extension = page_manual_section(output_type);
	    char *linkname = page_file_name(page->declarator->name,
							output_type, extension);
	    int result = 0;

	    /* we may have a function with the same name as the sourcefile */
	    if (!strcmp(filename, linkname))
	    {
		free(linkname);
		continue;
	    }

	    fprintf(stderr,"%s: %s\n",
		link_type == LINK_REMOVE ? "removing" : "linking", linkname);

	    /* always nuke old output file, since it may be linked to the one
	     * we've just generated, so LINK_FILE may trash it.
	     */
	    if (!remove_old_file(linkname))	exit(1);

	    switch(link_type)
	    {
#ifdef HAS_LINK
	    case LINK_HARD:
		result = link(filename, linkname);
		break;
#endif
#ifdef HAS_SYMLINK
	    case LINK_SOFT:
		result = symlink(filename, linkname);
		break;
#endif
	    case LINK_FILE:
		if (freopen(linkname, "w", stdout) == NULL)
		{
		    result = -1;
		    break;
		}
		output_warning();
		output->include(filename);
		if (fclose(stdout) == EOF)
		    result = -1;
		break;
	    case LINK_NONE:
	    case LINK_REMOVE:
		break;
	    }

	    /* check it went OK */
	    if (result == -1)
	    {
		my_perror("error linking output file", linkname);
		exit(1);
	    }
	    free(linkname);
	}
    }

    safe_free(filename);
}
