/* generate glassian site
                              _ _
          __ _  ___ _ __  ___(_) |_ ___        ___
         / _` |/ _ \ '_ \/ __| | __/ _ \      / __|
        | (_| |  __/ | | \__ \ | ||  __/  _  | (__
         \__, |\___|_| |_|___/_|\__\___| (_)  \___|
         |___/

   takes set of meta-html .h files organized into a heirarchy of .ring
   files and generates final html for each page.  also produces a manifest,
   site map, index, list of comparable insulator images, and list of external
   links.  converted from awk, which program was 1/2 the size; long live awk!

   ian macky feb 2004
*/

#include <ctype.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <jpeglib.h>
#include <tiffio.h>
#include "jpgmean.h"
#include <png.h>

/* ------------------------------- basic defines --------------------------- */

#ifndef TRUE
# define TRUE  1
# define FALSE 0
#endif

#define MAX_LINE		1024	/* input line */
#define MAX_PATH		512	/* filesystem path */
#define MAX_HASH		4001	/* hash table size MUST BE PRIME */

/* Title is specified as Long|Short, e.g. "Full Title about Thingy|Thingy": */

#define MAX_FIELD		1024	/* "Full Title about Thingy" */
#define MAX_SHORT_FIELD		64	/* "Thingy" */

#define MAX_REL_DEPTH		16	/* max path depth for rel_path() */
#define MAX_PAGE_PATH		4	/* show this many levels up */
#define MAX_PAGE_RING		16	/* max rings introduced by page */
#define MAX_LIB_ROW		10	/* max entries in a library row */

/* -------------------------------- site config ---------------------------- */

#ifndef GENSITE_NAME
# define GENSITE_NAME		"Site Name"
#endif

#ifndef GENSITE_WIDTH
# define GENSITE_WIDTH		"\"100%\""
#endif

#ifndef GENSITE_HEIGHT
# define GENSITE_HEIGHT		600
#endif

#ifndef GENSITE_FONT
# define GENSITE_FONT		"helvetica"
#endif

#ifndef GENSITE_FIXED
# define GENSITE_FIXED		"monospace"
#endif

#ifndef GENSITE_SANS
# define GENSITE_SANS		"sans"
#endif

/* ------------------------------------------------------------------------- */

#define ROOT_PAGE		"index.h"
#define ROOT_HTML		ROOT_PAGE "tml"
#define ROOT_RING		"root.ring"

#define LIBRARY_PAGE		"library.h"

#define RING_MAP		"rings.dat"	/* generated page-ring map */
#define SITE_RING		"site.ring"	/* ring with index & sitemap */
#define WEBRING			"webring"	/* their code to insert */
#define IMAGES			"images"	/* subdir for basic images */

#define SITEMAP			"sitemap.h"
#define SITEMAP_HTML		SITEMAP "tml"
#define SITEMAP_NAV		"Site Map"
#define SITEMAP_LONG		SITEMAP_NAV
#define SITEMAP_SHORT		"Map"
#define SITEMAP_TITLE		SITEMAP_LONG "|" SITEMAP_SHORT

#define INDEX			"master.h"	/* "index.h" obviously taken */
#define INDEX_HTML		INDEX "tml"
#define INDEX_NAV		"Index"
#define INDEX_LONG		INDEX_NAV
#define INDEX_SHORT		"Index"
#define INDEX_TITLE		INDEX_LONG "|" INDEX_SHORT
#define INDEX_LETTERS		26		/* index 'A' - 'Z' */

#define INDEXBAR_PADDING	4		/* see generate_index() */
#define INDEXBAR_SPACING	2

#define XLINK			"xlinks"	/* external links */
#define CMP_LIST		"cmplist"	/* comparable images */
#define MANIFEST		"manifest"	/* list of all files */
#define ERRATA			"errata"	/* extra stuff for tarball */
#define EVERY_PAGE		0		/* 0 if don't want */

#define LR_GIF			"lr.gif"	/* left-right arrows */
#define LRINV_GIF		"lrinv.gif"	/* lr inverse video */
#define LR0_GIF			"lr0.gif"	/* lr grayed out */
#define LR0INV_GIF		"lr0inv.gif"	/* lr inverse grayed out */
#define NAV_GIF			"nav.gif"	/* main navigation gif */
#define INVNAV_GIF		"navinv.gif"	/* inverse navigation gif */
#define NAVGIF_WIDTH		64		/* all of the navigation */
#define NAVGIF_HEIGHT		34		/* gifs are this size */

#define BOOK_SEPARATION		20	/* spacing between left/right */
#define RINGSEL_PADDING		4

#define MAX_MEMBERS		300		/* see ring_navigation */
#define MAX_SECTION		75		/* max pages/section */
#define RINGNAV_ROWS		30

/* -------------------------------- meta-data ------------------------------ */

#define CONTENT_TYPE		"text/html"
#define LANGUAGE		"en-US"
#define CHARSET			"ASCII"

#define DOCTYPE \
    "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">"

/* ------------------------------- font sizes ------------------------------ */

#define NOTE_SIZE		2

#define HEADING_SIZE		3
#define RINGHEAD_SIZE		3
#define ROOTNAV_SIZE		3
#define SELECTOR_SIZE		3

#define BOOK_SIZE		4
#define CDCMP_SIZE		"\"+1\""

#define RINGNAV_SIZE		3

/* ---------------------------------- colors ------------------------------- */

#define BODY_COLOR		"white"
#define TEXT_COLOR		"black"
#define LINK_COLOR		"black"
#define VLINK_COLOR		"black"

#define INVERSE_BODY_COLOR	"black"
#define INVERSE_TEXT_COLOR	"white"
#define INVERSE_LINK_COLOR	"white"
#define INVERSE_VLINK_COLOR	"white"
#define INVERSE_THEME_COLOR	"black"

#define THEME_COLOR		"\"#CCCCCC\""
#define INDEX_COLOR		"#FF9900"
#define CMP_COLOR		"\"#FFCC00\""

#define BOOK_BG			"\"#FFCC00\""

/* ---------------------------------- style -------------------------------- */

#define PAGE_STYLE		PAGE_STYLE_DEFAULT
#define RINGNAV_STYLE		STYLE_BULLET

#define RINGNAV_BULLET1		"square"
#define RINGNAV_BULLET2		"disc"
#define RINGNAV_HL_BULLET1	"disc"
#define RINGNAV_HL_BULLET2	"circle"

#define ROOT_CELLPADDING	2

/* ------------------------------------ cd --------------------------------- */

#define CD_DATA_FORMAT		"CD/%s/%s.dat"

#define CD_FORMAT \
    "<tr><th align=right><font face=helvetica>%s</font></th><td>%s</td></tr>"

#define CDCMP_FORMAT \
    "<tr align=center>\n" \
    "<th align=right><font face=%s size=%s>%s</font></th>\n" \
    "<td bgcolor=\"#CCCCCC\"><font face=%s size=%s>%s</font></td>\n" \
    "<td><big><b>&middot;</b></big></td>\n" \
    "<td bgcolor=\"#CCCCCC\"><font face=%s size=%s>%s</font></td>\n" \
    "</tr>\n"

/* ---------------------------------- markup ------------------------------- */

#define EOG_MARKER		"  <!-- EOG -->\n" /* End of Generated */
#define SOG_MARKER		"  <!-- SOG -->\n" /* Start of Generated */

#define AREA_FORMAT		"    <area href=\"%s\" %s>"
#define BODY_FORMAT		"<body %s text=%s link=%s vlink=%s>\n"

/* ------------------------------- meta markup ---------------------------- */

#define META_LINK_FORMAT	"@@link{%s %s}"

/* -------------------- html emtities to convert in index ----------------- */

#define INDEX_KEYSTRIP		" .,-_'\"&()"

#define INDEX_ENTITY(e, a)	{ e, sizeof(e) - 1, a }

struct INDEX_ENTITY {
    char *entity;
    size_t entlen;
    char *ascii;
} index_entities[] = {
    INDEX_ENTITY("aacute;",	"a"),
    INDEX_ENTITY("aelig;",	"ae"),
    INDEX_ENTITY("amp;",	""),
    INDEX_ENTITY("auml;",	"a"),
    INDEX_ENTITY("ccedil;",	"c"),
    INDEX_ENTITY("eacute;",	"e"),
    INDEX_ENTITY("egrave;",	"e"),
    INDEX_ENTITY("middot;",	""),
    INDEX_ENTITY("ntilde;",	"n"),
    INDEX_ENTITY("oelig;",	"oe"),
    INDEX_ENTITY("ordm;",	"o"),
    INDEX_ENTITY("scaron;",	"s"),
    INDEX_ENTITY("sup1;",	"1"),
    INDEX_ENTITY("sup2;",	"2"),
    INDEX_ENTITY("trade;",	""),
    INDEX_ENTITY("uuml;",	"u"),
    { NULL }
};

/* ---------------------------------- flags ------------------------------- */

#define BIT(n)		(((unsigned) 1) << (n))

/* Next level, structs with 'flags' member */

#define ON(p, w)	(((p)->flags & (w)) != 0)
#define OFF(p, w)	(((p)->flags & (w)) == 0)

#define SET(p, w)	((p)->flags |=  (w))
#define CLR(p, w)	((p)->flags &= ~(w))
#define XOR(p, w)	((p)->flags ^=  (w))

/* ------------------------------- linked list ---------------------------- */

typedef struct {
    void  *head;		/* head of list */
    void  *tail;		/* tail of list */
    int n;			/* length of list */
} list;

#define LINK_GLUE(TYPE)		struct TYPE *next, *prev

#define LIST_HEAD(l)		(l)->head
#define LIST_TAIL(l)		(l)->tail
#define LIST_SIZE(l)		(l)->n

#define LINK_NEXT(thing)	(thing)->next
#define LINK_PREV(thing)	(thing)->prev

#define LIST_INIT(l) { \
    LIST_HEAD(l) = LIST_TAIL(l) = NULL; \
    LIST_SIZE(l) = 0; \
}

#define LINK_HEAD(l, thing) { \
    if ((LINK_NEXT(thing) = LIST_HEAD(l))) \
	LINK_PREV(LINK_NEXT(thing)) = (thing); \
    LINK_PREV(thing) = NULL; \
    LIST_HEAD(l) = (thing); \
    if (!LIST_TAIL(l)) \
	LIST_TAIL(l) = (thing); \
    LIST_SIZE(l)++; \
}

#define LINK_TAIL(l, thing) { \
    if ((LINK_PREV(thing) = LIST_TAIL(l))) \
	LINK_NEXT(LINK_PREV(thing)) = (thing); \
    LINK_NEXT(thing) = NULL; \
    LIST_TAIL(l) = (thing); \
    if (!LIST_HEAD(l)) \
	LIST_HEAD(l) = (thing); \
    LIST_SIZE(l)++; \
}

#define LINK_BEFORE(l, before, thing) { \
    LINK_NEXT(thing) = (before); \
    if ((LINK_PREV(thing) = LINK_PREV(before))) \
	LINK_NEXT(LINK_PREV(before)) = (thing); \
    else \
	LIST_HEAD(l) = (thing); \
    LINK_PREV(before) = (thing); \
    LIST_SIZE(l)++; \
}

/* ----------------------------------- types ------------------------------ */

#ifndef JPEGLIB_H
typedef int boolean; 	/* jpeglib.h defines one too */
#endif

typedef enum {
    PAGE_STYLE_DEFAULT = 0,
    PAGE_STYLE_BOOK,
    PAGE_STYLE_VERBATIM,
} pstyle;

typedef enum {
    RING_STYLE_DEFAULT = 0,
    RING_STYLE_BOOK
} rstyle;

#define PROCESS_OPEN		BIT(0)

#define RING_PROCESSED		BIT(0)

#define STYLE_BULLET		BIT(0)
#define STYLE_CITE		BIT(1)
#define STYLE_DATE		BIT(2)
#define STYLE_LI		BIT(3)
#define STYLE_NEXTPREV		BIT(4)
#define STYLE_SANS		BIT(5)
#define STYLE_SELECT		BIT(6)
#define STYLE_SHORT		BIT(7)
#define STYLE_10OPCT		BIT(8)
#define STYLE_BOLD		BIT(9)

/* ----------------------------------- ring ------------------------------ */

struct PAGE; /* forward decl */

typedef struct RING {
    unsigned flags;			/* mask of flags */
    rstyle   style;			/* basic style */
    char     path[MAX_PATH];		/* "foo.ring" */
    char     long_title[MAX_FIELD];	/* full long title */
    char     short_title[MAX_SHORT_FIELD]; /* short abbreviated title */
    char    *cite;			/* optional citation */
    char    *theme;			/* theme/title-bar color */
    char    *bookbg;			/* book style body color */
    char    *bookpage;			/* book style page background */
    char    *width, *height;		/* override global default */
    int      cols;			/* forced # cols for ringnav */
    int      size;			/* font size */
    list     members;			/* all ring member pages */
    struct PAGE *page;			/* page which introduces ring */
} ring;

/* ----------------------------------- page ------------------------------ */

#define PAGE_IS_ROOT		BIT(0)	/* is this ROOT_PAGE? */
#define PAGE_IS_LIBRARY		BIT(1)	/* is this tge library? */
#define PAGE_PROCESSED		BIT(2)	/* already processed this .h? */
#define PAGE_INVERSE		BIT(3)	/* reverse colors? */
#define PAGE_SECTION		BIT(4)	/* page starts new section */

/* forward decl */

struct MEMBER; typedef struct MEMBER member;
struct PAGE;   typedef struct PAGE page;

struct PAGE {
    FILE    *in;			/* reading .h */
    FILE    *out;			/* producing .html */
    unsigned flags;			/* page flags */
    pstyle   style;
    ring    *ring;			/* page belongs to this ring */
    page    *up;			/* up goes to this page */
    char    *width, *height;		/* override site defaults */
    int      ord;			/* ring ordinal */
    int      nms;			/* next/prev map count */
    int      meta_lines;		/* # of lines of @metadata */
    /* real storage for these since every page has them */
    char     h[MAX_PATH];		/* foo.h */
    char     html[MAX_PATH];		/* foo.html */
    char     long_title[MAX_FIELD];	/* full (long) title */
    char     short_title[MAX_SHORT_FIELD]; /* short title */
    /* ptr to malloc'd for less frequent stuff */
    char    *theme;			/* theme/title-bar color */
    char    *bookpage;			/* override ring's */
    char    *background;		/* page background image */
    char    *bg;			/* page background color */
    char    *cite;			/* citation */
    char    *date;			/* date of original */
    char    *keywords;			/* <meta> keywords */
    char    *description;		/* <meta> description */
    char    *css;			/* internal stylesheet */
    char    *thumb;			/* thumbnail for page scan */
    char    *thumb_left;		/* same in alt position */
    char    *thumb_left2;		/* second one */
    char    *tthumb_left;		/* smaller thumb */
    char    *thumb_11;			/* 1:1 thumb (full res) */
    int      n_rings;			/* page introduces n rings */
    ring    *rings[MAX_PAGE_RING];	/* only this many allowed */
    int      n_ringnavs;
    ring    *ringnavs[MAX_PAGE_RING];	/* page wants nav for these rings */
    int      ringnavsize[MAX_PAGE_RING];/* font size */
    int      ringnav_size;		/* ringnav font size */
    int      ringnav_cols;		/* ringnav # columns */
    long     body;			/* position of body start */
    member  *member;			/* backpointer */
};

/* ------------------------------ member of ring --------------------------- */

struct MEMBER {
    page *p;		/* member may be a page... */
    ring *r;		/* ...or another ring */
    LINK_GLUE(MEMBER);
};

/* ------------------------------------ image ------------------------------ */

/* image file and its metadata */

typedef struct IMAGE {
    char    path[MAX_PATH];
    int     width, height;
    int     cmpnum;		/* comparator {number} */
    char   *colorname;		/* name of color, if a comparable image */
    JSAMPLE red, green, blue;	/* average color */
    FILE   *f;
    LINK_GLUE(IMAGE);
    struct IMAGE *thumb_of;	/* this is a thumb of... */
} image;

/* ------------------------------------ index ------------------------------ */

/* index entry */

typedef struct ENTRY {
    char   *tag;		/* unique target */
    char   *text;		/* full text */
    char   *primary;		/* primary key */
    char   *primary_sort;	/* text w/tags removed, all lowercase */
    char   *secondary;		/* secondary key */
    char   *secondary_sort;	/* text w/tags removed, all lowercase */
    char   *comment;		/* parenthetical comment */
    page   *page;		/* points to this page */
    boolean main;		/* this the main entry? */
    boolean image;		/* this an image entry? */
    char   *see;
    LINK_GLUE(ENTRY);
} entry;

/* ---------------------------------- CD data ------------------------------ */

/* data for a specific CD */

typedef struct {
    char width[MAX_SHORT_FIELD];
    char height[MAX_SHORT_FIELD];
    char weight[MAX_SHORT_FIELD];
    char voltage[MAX_SHORT_FIELD];
    char leakage[MAX_SHORT_FIELD];
    char pinhole[MAX_SHORT_FIELD];
    char style[MAX_SHORT_FIELD];
} cd_data;

/* --------------------------------- hash table ---------------------------- */

/* hash table bucket */

typedef struct BUCKET {
    char *key;
    void *data;
    struct BUCKET *next;		/* no need for double-link */
} bucket;

typedef bucket *hash[MAX_HASH];

/* -------------------------------- @key processor ------------------------- */

#define BODY_KEYFUNC(func, p, up, args) \
    boolean func(page *p, page *up, char *args)

BODY_KEYFUNC(body_key_area,        p, up, args);
BODY_KEYFUNC(body_key_cd,          p, up, args);
BODY_KEYFUNC(body_key_cdcmp,       p, up, args);
BODY_KEYFUNC(body_key_cellbg,      p, up, args);
BODY_KEYFUNC(body_key_font,        p, up, args);
BODY_KEYFUNC(body_key_fixed,       p, up, args);
BODY_KEYFUNC(body_key_sans,        p, up, args);
BODY_KEYFUNC(body_key_image,       p, up, args);
BODY_KEYFUNC(body_key_image_page,  p, up, args);
BODY_KEYFUNC(body_key_image_link,  p, up, args);
BODY_KEYFUNC(body_key_thumb_image, p, up, args);
BODY_KEYFUNC(body_key_thumb_link,  p, up, args);
BODY_KEYFUNC(body_key_thumb,       p, up, args);
BODY_KEYFUNC(body_key_tthumb,      p, up, args);
BODY_KEYFUNC(body_key_lib_row,     p, up, args);
BODY_KEYFUNC(body_key_link,        p, up, args);
BODY_KEYFUNC(body_key_index,       p, up, args);
BODY_KEYFUNC(body_key_index_main,  p, up, args);
BODY_KEYFUNC(body_key_index_image, p, up, args);
BODY_KEYFUNC(body_key_ringnav,     p, up, args);
BODY_KEYFUNC(body_key_ringhead,    p, up, args);
BODY_KEYFUNC(body_key_webring,     p, up, args);
BODY_KEYFUNC(body_key_width,       p, up, args);
BODY_KEYFUNC(body_key_xlink,       p, up, args);
BODY_KEYFUNC(body_key_xlink_image, p, up, args);

struct {
    char         *key;
    BODY_KEYFUNC((*func), p, up, args);
} body_keyfuncs[] = {
    { "area",		body_key_area },
    { "cellbg",		body_key_cellbg },
    { "cd",		body_key_cd },
    { "cdcmp",		body_key_cdcmp },
    { "fixed",		body_key_fixed },
    { "font",		body_key_font },
    { "image",		body_key_image },
    { "image-page",	body_key_image_page },
    { "image-link",	body_key_image_link },
    { "index",		body_key_index },
    { "index-main",	body_key_index_main },
    { "index-image",	body_key_index_image },
    { "librow",		body_key_lib_row },
    { "link",		body_key_link },
    { "ringnav",	body_key_ringnav },
    { "ringhead",	body_key_ringhead },
    { "sans",		body_key_sans },
    { "thumb-image",	body_key_thumb_image },
    { "thumb-link",	body_key_thumb_link },
    { "thumb",		body_key_thumb },
    { "tthumb",		body_key_tthumb },
    { "webring",	body_key_webring },
    { "width",		body_key_width },
    { "xlink",		body_key_xlink },
    { "xlink-image",	body_key_xlink_image }
};

#define N_KEYFUNCS	(sizeof(body_keyfuncs) / sizeof(body_keyfuncs[0]))

/* ---------------------------------- macros ------------------------------ */

#define FCLOSE(f) { fclose(f); f = NULL; }

#define STR(s) ((s) ? (s) : "NIL")

#define CALLOC(what, thing) \
    if (!(what = calloc(1, sizeof(thing)))) \
	{ puts("\nOUT OF MEMORY"); exit(2); }

#define MALLOC(what, thing) \
    if (!(what = malloc(sizeof(thing)))) \
	{ puts("\nOUT OF MEMORY"); exit(2); }

/* ---------------------------------- globals ------------------------------ */

int     debug;			/* debug level, 0 = none */
int	level;			/* level in heirarchy */
boolean loading;		/* doing main load?  not post-processing? */
boolean single_page;		/* single-page development mode */

page   *root;			/* root page of site, ROOT_PAGE */
hash    pages;			/* hash table of all pages */
int     n_pages;		/* total number of pages */

ring   *root_ring;		/* top-level site ring */
hash    rings;			/* hash table of all rings */
int     n_rings;		/* total number of rings */
FILE   *ring_map;		/* generated page-ring map */

list    entries[INDEX_LETTERS];	/* lists of index entries, one per letter */
int     n_index;		/* total # of index entries */
page   *index_page;		/* index() is a string.h func */

hash    images;			/* hash table of all images */
int     n_images;		/* total number of images */

list    cmplist;		/* list of all comparable insulator images */
boolean new_colors;		/* any new comparables added this run? */

hash    tags;			/* index targets */

int     n_cds;			/* number of special CD pages */

page   *sitemap;		/* generated sitemap */
FILE   *xlinks;			/* list of all external links */

FILE   *manifest;		/* generated list of all site files */
int     n_manifest;		/* total # of files in manifest */

page   *cur_page;		/* current page being processed */

/* ----------------------- private function declarations ------------------- */

boolean  body_key_index_worker(page *p, page *up, char *args,
			       boolean main, boolean image, boolean top);
boolean  body_key_thumbn(page *p, page *up, char *args, int n_t);
boolean  body_process(page *p, page *up);
boolean  comparable(image *i, char *colorname);
char    *dest_parse(char *dest, char *rp, page *p, page *up, int relp);
boolean  do_body_key(page *p, page *up, char *key, char *args);
char    *find_index(char *name, char *dest);
member  *find_member(list *members, page *p, int *ord);
int      generate_index(void);
char    *get_params(char *from, char *dest);
boolean  gif_size(image *i);
void     hash_add(hash h, char *key, void *data);
void     hash_dump(hash h);
void    *hash_lookup(hash h, char *key);
unsigned hash_of(char *s);
void     hash_stats(hash h);
boolean  has_extension(char *s, char *ext);
image   *image_register(char *ref, char *image_path);
boolean  image_rgb(image *i);
boolean  image_write(page *p, char *img, char *to, char *tag,
			char *args, int n_thumb);
boolean  jpg_size(image *i);
boolean  line_process(page *p, page *up, char *line, int line_no);
boolean  load_cd_data(char *cd, cd_data *cdd);
boolean  load_cmplist(void);
char    *lowercaseify(char *buf);
char    *next_arg(char *line, char *buf, char **rest);
page    *page_register(char *ref, char *h_path, char *def_theme,
			boolean primary, boolean solo, char *tag);
boolean  page_meta(page *p, boolean primary, boolean solo);
void     page_path(page *p, page *cur, char *pathbuf, int depth);
boolean  page_process(page *p, page *up, ring *r, unsigned flags);
int      path_split(char *path, char *pathbuf, char *parts[]);
boolean  png_size(image *i);
boolean proc_lib(page *p, page *up, char *id, FILE *f, boolean first);
char    *rel_path(char *from, char *to, char *buf);
char    *rel_expand(char *rel, char *ref, char *buf);
page    *ring_firstpage(ring *r);
page    *ring_lastpage(ring *r);
boolean  ring_meta(ring *r, boolean primary, boolean solo);
boolean  ring_navigation(page *p, ring *r, page *up, unsigned style,
			int size, int cols, char *font);
boolean  ring_nextprev(page *p, ring *r);
boolean  ring_process(ring *r, page *up);
ring    *ring_register(char *ref, char *ring_path,
			char *def_theme, boolean primary, boolean solo);
boolean  ring_selector(page *p, ring *r, page *up, int size, int cols);
char    *save_string(char *s);
void     strip_tags(char *from, char *dest);
unsigned style_mask(char *s);
char    *sub_bigger(char *old, char *new, char *buf, char *newbuf);
void     sub_smaller(char *old, char *new, char *buf);
boolean  tif_size(image *i);
boolean  write_cmplist(void);
boolean  write_link(char *link_href, char *params, char *link_text, page *p);
void     write_meta(page *p, ring *r);
boolean  write_navigation(page *p, page *up, ring *r);

/* ----------------------------------- main -------------------------------- */

int main(int argc, char *argv[])
{
    page   *p;
    ring   *site_ring, *r;
    char    path[MAX_FIELD], line[MAX_LINE];
    char   *arg, *switches;
    FILE   *errata;
    int     sw, n_errata;

    puts("gensite.c $Revision: 1.157 $");

    /* process switches */
    for (argv++, argc--; (arg = *argv) && (*arg == '-'); argv++, argc--)
    {
	switches = arg + 1;
	while ((sw = *switches++))
	{
	    switch (sw)
	    {
		case 'x':
		    debug++;	/* the more x's, the more detail */
		    break;

		default:
		    printf("Unknown switch '%c'.\n", sw);
		    return 1;
	    }
	}
    }

    /* development shortcut to rebuild a single page; will have bogus nav */
    if (argc > 1)
    {
	if (argc != 2)
	{
	    puts("usage: gensite <page> <ring>");
	    return 1;
	}
	single_page = TRUE;
	if (debug)
	    puts("Single-page mode");
	if (!(p = page_register(NULL, argv[0], NULL, TRUE, TRUE, NULL)))
	    return 1;
	if (ON(p, PAGE_IS_ROOT))
	    r = NULL;
	else
	{
	    if (!(r = ring_register(NULL, argv[1], NULL, TRUE, TRUE)))
		return 1;
	    if (!ring_meta(r, TRUE, TRUE))
		return 1;
	}
	return page_process(p, NULL, r, TRUE) ? 0 : 1;
    }

    printf("Generating '%s'...\n", NAME);

    /* -------------------------- start rings list ------------------------- */

    if (!(ring_map = fopen(RING_MAP, "w")))
	return 1;

    /* --------------------------- start site map -------------------------- */

    if (!(sitemap = page_register(NULL, SITEMAP, NULL, TRUE, FALSE, NULL)))
	return 1;
    strcpy(sitemap->long_title, SITEMAP_LONG);
    strcpy(sitemap->short_title, SITEMAP_SHORT);
    SET(sitemap, PAGE_PROCESSED);
    if (!(sitemap->out = fopen(sitemap->h, "w"))) /* note: we make a .h! */
    {
	printf("Failed to open sitemap '%s'\n", path);
	return 1;
    }
    fprintf(sitemap->out, "@title %s\n\n", sitemap->long_title);
    sitemap->body = ftell(sitemap->out);
    fputs("@index S Site map\n", sitemap->out);
    fputs("<ul>\n", sitemap->out);

    /* --------------------------- start index ----------------------------- */

    if (!(index_page = page_register(NULL, INDEX, NULL, TRUE, FALSE, NULL)))
	return 1;
    strcpy(index_page->long_title, INDEX_LONG);
    strcpy(index_page->short_title, INDEX_SHORT);
    SET(index_page, PAGE_PROCESSED);
    if (!(index_page->out = fopen(index_page->h, "w")))
    {						/* note: we make a .h! */
	printf("Failed to open index '%s'\n", path);
	return 1;
    }
    fprintf(index_page->out, "@title %s\n\n", index_page->long_title);
    index_page->body = ftell(index_page->out);

    /* --------------------- start manifest with errata -------------------- */

    if (debug)
	printf("Starting manifest '%s'\n", MANIFEST);
    if (!(manifest = fopen(MANIFEST, "w")))
    {
	printf("Failed to open manifest '%s'\n", MANIFEST);
	return 1;
    }

    if (debug)
	printf("Reading errata file '%s'\n", ERRATA);
    n_errata = 0;
    if ((errata = fopen(ERRATA, "r")))
    {
	while (fgets(line, sizeof(line), errata))
	{
	    if (*line != '#')
	    {
		fputs(line, manifest);
		n_manifest++;
		n_errata++;
	    }
	}
	FCLOSE(errata)
    }

    /* ---------------------- start external links list -------------------- */

    if (debug)
	printf("Opening external links file '%s'\n", XLINK);
    if (!(xlinks = fopen(XLINK, "w")))
    {
	printf("Failed to open xlink '%s'\n", XLINK);
	return 1;
    }

    /* -------------------------- start images list ------------------------ */

    if (!load_cmplist())
	return 1;

    /* -------------------- register the root ring ------------------------- */

    if (!(root_ring = ring_register(NULL, ROOT_RING, NULL, TRUE, FALSE)))
    {
	printf("Failed to load root ring '%s'\n", ROOT_RING);
	return 1;
    }

    /* ----------------------- ditto the site ring ------------------------- */

    if (!(site_ring = ring_register(NULL, SITE_RING, NULL, TRUE, FALSE)))
    {
	printf("Failed to load site ring '%s'\n", SITE_RING);
	return 1;
    }

    /* --------------- start recursing from the root page ------------------ */

    printf("Starting at root page '%s'...\n", ROOT_PAGE);
    if (!(root = page_register(NULL, ROOT_PAGE, NULL, TRUE, FALSE, NULL)))
	return 1;
    loading = TRUE;
    if (!page_process(root, NULL, NULL, FALSE))
    {
	printf("Processing failed for '%s'\n", root->h);
	if (new_colors)		/* save the cmplist! */
	    write_cmplist(); 	/* computing average RGB is expensive */
	return 1;
    }
    loading = FALSE;

    /* ------------------- process finished sitemap ------------------------ */

    fputs("</ul>\n", sitemap->out);
    FCLOSE(sitemap->out)
    CLR(sitemap, PAGE_PROCESSED);			/* force reload */
    printf("  Processing site map %s\n", sitemap->h);
    if (!page_process(sitemap, root, site_ring, 0))
    {
	puts("Sitemap processing failed");
	return 1;
    }

    /* ----------------- generate index, then process it ------------------- */

    printf("  Generating index %s\n", index_page->h);
    if (!generate_index())
    {
	puts("Index generation failed.");
	return 1;
    }
    FCLOSE(index_page->out)
    CLR(index_page, PAGE_PROCESSED);			/* force reload */
    puts("  Processing index");
    if (!page_process(index_page, root, site_ring, 0))
    {
	puts("Index processing failed");
	return 1;
    }

    /* -------------- finished with all pages, close the rest -------------- */

    FCLOSE(manifest)
    FCLOSE(xlinks)
    FCLOSE(ring_map)

    /* write out updated color comparison list if additions */
    if (new_colors)
	write_cmplist();

    printf("  %d files: %d errata + %d pages in %d rings\n",
	n_manifest, n_errata, n_pages, n_rings);
    printf("  %d comparable images out of %d total\n",
	LIST_SIZE(&cmplist), n_images);

    if (debug)
    {
	fputs("Pages hash stats:  ", stdout);
	hash_stats(pages);
	fputs("Rings hash stats:  ", stdout);
	hash_stats(rings);
	fputs("Images hash stats: ", stdout);
	hash_stats(images);
    }

    puts("Done.");
    return 0;
}

void add_to_manifest(char *file)
{
    if (manifest)
    {
	fputs(file, manifest);
	putc('\n', manifest);
	n_manifest++;
    }
}

/* --------------------------------- page -------------------------------- */

/* return page pointer given its path.  pages are identified by the
   full path to their .h file.  if the named page already exists, it
   is just returned.  otherwise, adds a new page.  pages names are in
   the global hash table 'pages'.
 */

page *page_register(char *ref, char *h_path, char *def_theme,
		    boolean primary, boolean solo, char *tag)
{
    static char pathbuf[MAX_PATH];
    char *hash;
    page *p;

    rel_expand(h_path, ref, pathbuf);		/* cvt rel path to abs */

    if (tag)
	*tag = 0;
    if ((hash = strchr(pathbuf, '#')))
    {
	*hash++ = 0;
	if (tag)
	    strcpy(tag, hash);
    }

    if ((p = hash_lookup(pages, pathbuf)))	/* already exists? */
    {
	if (debug > 4)
	    fprintf(stderr, "page_register(ref='%s', h_path='%s') -> %s\n",
			    ref, h_path, p->h);
	return p;				/* yes, return old page. */
    }

    if (debug > 2)
	printf("page register '%s'\n", pathbuf);

    CALLOC(p, page)

    strcpy(p->h, pathbuf);			/* "foo.h" */
    strcpy(p->html, p->h);
    strcat(p->html, "tml");			/* "foo.html" */

    p->theme = def_theme;

    add_to_manifest(p->html);

    /* root page path only matches for top-level "index.h", not some
       sub-page "index.h" that's being worked on.
    */
    if (!strcmp(h_path, ROOT_PAGE))
	SET(p, PAGE_IS_ROOT);

    /* library is a special case to avoid @librow duplicate index/id */
    if (!strcmp(h_path, LIBRARY_PAGE))
	SET(p, PAGE_IS_LIBRARY);

    hash_add(pages, p->h, (void *) p);
    n_pages++;

    if (!page_meta(p, primary, solo))		/* load page's metadata */
	return NULL;				/* doesn't exist or bogus */

    return p;					/* return new page */
}

/* load page metadata */

boolean page_meta(page *p, boolean primary, boolean solo)
{
    char   line[MAX_LINE], rp[MAX_FIELD], key[MAX_FIELD], height[MAX_FIELD],
	   width[MAX_FIELD], path[MAX_FIELD], argbuf[MAX_FIELD];
    char  *rest, *e, *bar;
    int    len, line_no, size;
    pstyle s;
    ring  *r;

    if (debug > 3)
	fprintf(stderr, "-->   page_meta(p='%s')\n", p->h);

    p->style = PAGE_STYLE;

    if (loading)
    {
	/* special cases for generated files */
	if (p == sitemap)
	{
	    strcpy(p->long_title, SITEMAP_LONG);
	    strcpy(p->short_title, SITEMAP_SHORT);
	    return TRUE;
	}
	if (p == index_page)
	{
	    strcpy(p->long_title, INDEX_LONG);
	    strcpy(p->short_title, INDEX_SHORT);
	    return TRUE;
	}
    }

    /* open the .h meta-html file */
    if (!(p->in = fopen(p->h, "r")))
    {
	printf("***** ERROR: failed to open h file '%s'\n", p->h);
	perror("fopen");
	return FALSE;
    }

    /* read the header, stop at blank line */
    for (line_no = 1; fgets(line, sizeof(line), p->in); line_no++)
    {
	if (*line == '#')			/* ignore #comments */
	    continue;

	/* clobber ending NL */
	e = line + strlen(line) - 1;		/* last char */
	if (*e != '\n')
	{
	    fprintf(stderr, "line %d overflow; max %d\n",
			line_no, (int) sizeof(line));
	    return FALSE;
	}
	*e = 0;					/* clobber NL */

	/* blank line ends header */
	if (!*line)
	    break;

	next_arg(line, key, &rest);		/* get first arg as <key> */
	if (!strcmp(key, "@title"))
	{
	    if ((bar = strchr(rest, '|')))	/* "Long Wordy Title|Title" */
	    {
		len = bar - rest;
		strncpy(p->long_title, rest, len);
		p->long_title[len] = 0;
		strcpy(p->short_title, bar + 1);
	    }
	    else
	    {
		strcpy(p->long_title, rest);
		strncpy(p->short_title, rest, MAX_SHORT_FIELD);
		p->short_title[MAX_SHORT_FIELD - 1] = 0;
	    }
	}
	else if (!strcmp(key, "@inverse"))	/* invert colors? */
	    SET(p, PAGE_INVERSE);
	else if (!strcmp(key, "@css"))
	    p->css = save_string(rest);
	else if (!strcmp(key, "@style"))
	{
	    next_arg(rest, key, &rest);
	    if (!strcmp(key, "book"))
		s = PAGE_STYLE_BOOK;
	    else if (!strcmp(key, "vertical"))
		s = PAGE_STYLE_DEFAULT;
	    else if (!strcmp(key, "verbatim"))
		s = PAGE_STYLE_VERBATIM;
	    else
	    {
		printf("***** ERROR: page=%s line %d "
		       "unknown page style %s", p->h, line_no, key);
		return FALSE;
	    }

	    switch (p->style = s)
	    {
		case PAGE_STYLE_BOOK:
		    if (!next_arg(rest, path, &rest))
			return FALSE;
		    rel_expand(path, p->h, rp);
		    if (!(r = ring_register(p->h, rp, p->theme, primary,solo)))
			return FALSE;

		    if (rest)
		    {
			next_arg(rest, width, &rest);
			if (!atoi(width))
			{
			    printf("***** ERROR: page width '%s'.\n", width);
			    return FALSE;
			}
			p->width = save_string(width);

			next_arg(rest, height, &rest);
			if (!atoi(height))
			{
			    printf("***** ERROR: page height '%s'.\n", height);
			    return FALSE;
			}
			p->height = save_string(height);
		    }
		    else
		    {
			p->width = r->width;
			p->height = r->height;
		    }
		    break;

		default:
		    /* no extra arguments for the rest */
		    break;
	    }
	}
	else if (!strcmp(key, "@book-ringnav-size"))
	    p->ringnav_size = atoi(rest);
	else if (!strcmp(key, "@book-ringnav-cols"))
	    p->ringnav_cols = atoi(rest);
	else if (!strcmp(key, "@color"))
	    p->theme = save_string(rest);
	else if (!strcmp(key, "@background"))
	    p->background = save_string(rest);
	else if (!strcmp(key, "@bg"))
	    p->bg = save_string(rest);
	else if (!strcmp(key, "@bookpage"))
	    p->bookpage = save_string(rest);
	else if (!strcmp(key, "@cite"))
	    p->cite = save_string(rest);
	else if (!strcmp(key, "@date"))
	    p->date = save_string(rest);
	else if (!strcmp(key, "@section"))
	    SET(p, PAGE_SECTION);
	else if (!strcmp(key, "@ringnav"))
	{
	    if (!next_arg(rest, path, &rest))
		return FALSE;

	    if (rest)
	    {
		next_arg(rest, argbuf, &rest);
		size = atoi(argbuf);
		if (rest)
		    rel_expand(rest, p->h, rp);
	    }
	    else
		size = RINGNAV_SIZE;

	    if (p->n_ringnavs == MAX_PAGE_RING)
	    {
		printf("***** ERROR: page=%s line %d "
		       "too many ringnavs, max %d\n",
			p->h, line_no, MAX_PAGE_RING);
		return FALSE;
	    }
	    if (!(p->ringnavs[p->n_ringnavs] =
			ring_register(p->h, path, p->theme, primary, solo)))
		return FALSE;
	    p->ringnavsize[p->n_ringnavs++] = size;
	}
	else if (!strcmp(key, "@keywords"))
	    p->keywords = save_string(rest);
	else if (!strcmp(key, "@description"))
	    p->description = save_string(rest);
	else if (!strcmp(key, "@ring"))
	{
	    rel_expand(rest, p->h, rp);
	    if (p->n_rings == MAX_PAGE_RING)
	    {
		printf("***** ERROR: page '%s' line %d "
		       "too many rings, max %d\n",
			p->h, line_no, MAX_PAGE_RING);
		return FALSE;
	    }
	    if (!(r = ring_register(p->h, rp, p->theme, TRUE, solo)))
		return FALSE;
	    if (r->page)
	    {
		printf("***** ERROR: ring '%s' introduced by page '%s' "
			"*and* '%s'\n", r->path, r->page->h, p->h);
		return FALSE;
	    }
	    p->rings[p->n_rings++] = r;
	    r->page = p;			/* ring r introduced by p */
	}
	else if (!strcmp(key, "@thumb"))	/* thumbnail for page scan */
	    p->thumb = save_string(rest);
	else if (!strcmp(key, "@thumb-left"))	/* same in alt. position */
	{
	    if (!p->thumb_left)
		p->thumb_left = save_string(rest);
	    else if (!p->thumb_left2)
		p->thumb_left2 = save_string(rest);
	    else
	    {
		printf("***** ERROR: page '%s' max 2 thumb-lefts\n", p->h);
		return FALSE;
	    }
	}
	else if (!strcmp(key, "@tthumb-left"))	/* same in alt. position */
	{
	    if (!p->tthumb_left)
		p->tthumb_left = save_string(rest);
	}
	else if (!strcmp(key, "@thumb-1:1"))	/* full-res thumb*/
	{
	    if (!p->thumb_11)
		p->thumb_11 = save_string(rest);
	}
	else if (!strcmp(key, "@index"))	/* main index entry */
	{
	    if (!body_key_index_worker(p, NULL, rest, FALSE, FALSE, TRUE))
		return FALSE;
	}
	else if (!strcmp(key, "@index-main"))	/* main index entry */
	{
	    if (!body_key_index_worker(p, NULL, rest, TRUE, FALSE, TRUE))
		return FALSE;
	}
	else
	{
	    printf("***** ERROR: page '%s' line %d "
		   "unknown metadata key '%s'\n", p->h, line_no, key);
	    return FALSE;
	}
    }

    if (debug > 3)
	fprintf(stderr, "<--   page_meta(p='%s')\n", p->h);

    if (p != cur_page)		/* if not current page being processed */
    {
	p->body = ftell(p->in);	/* note body starts here; */
	FCLOSE(p->in)		/* come back to it later. */
    }

    p->meta_lines = line_no + 1; /* for correct body line# output */

    return TRUE;
}

/* register a new page */

boolean page_process(page *p, page *up, ring *r, unsigned flags)
{
    ring *pr;
    char *title;
    char  cd_title[MAX_FIELD];
    int   i;

    if (!p)
    {
	puts("***** ERROR: page_process: no page");
	return FALSE;
    }

    if (!r && OFF(p, PAGE_IS_ROOT))
    {
	puts("***** ERROR: page_process: no ring");
	return FALSE;
    }

    if (ON(p, PAGE_PROCESSED))
	return TRUE;			/* already processed this page */

    /* accumulate page-ring associations */
    if (p->h && ring_map)
	fprintf(ring_map, "%s %s\n", p->h, r ? r->path : "ROOT");

    level++;

    SET(p, PAGE_PROCESSED);		/* avoid loops */

    cur_page = p;

    if (debug)
	fprintf(stderr, "Processing page (%d) '%s'\n", level, p->h);

    p->up = up;

    if (!p->ring)
	p->ring = r;

    if (!(p->in = fopen(p->h, "r")))	/* re-open file */
    {
	printf("***** ERROR: failed to open h file '%s'\n", p->h);
	perror("fopen");
	goto page_error;
    }

    if (fseek(p->in, p->body, SEEK_SET))
    {
	printf("***** ERROR: failed to seek h file '%s' to %ld\n",
		    p->h, p->body);
	goto page_error;
    }

    if (!(p->out = fopen(p->html, "w")))
    {
	fprintf(stderr, "failed to open html file '%s'\n", p->html);
	goto page_error;
    }

    /* update sitemap */
    if (loading)
    {
	if (OFF(p, PAGE_IS_ROOT) && (r->style != RING_STYLE_BOOK))
	{
	    boolean is_cd = !strcmp(r->long_title, "Threaded Insulators");

	    if (is_cd && n_cds++)
		fputs(" \n", sitemap->out);
	    else
		fputs("<li>", sitemap->out);

	    if (is_cd)
	    {
		strcpy(title = cd_title, p->long_title);
		sub_smaller("CD ", "", title);
	    }
	    else
	    {
		title = p->long_title;
		if (!strncmp(title, GENSITE_NAME, sizeof(GENSITE_NAME) - 1))
		    title += sizeof(GENSITE_NAME) + 1;
	    }

	    if (level == 2)
		fputs("<big>", sitemap->out);
	    fprintf(sitemap->out, META_LINK_FORMAT, p->h, title);
	    if (level == 2)
		fputs("</big>", sitemap->out);

	    if (!is_cd)
		fputs("<br>\n", sitemap->out);
	}
    }

    /* write basic HTML metadata */
    write_meta(p, r);			/* write HTML metadata to .html */

    if (OFF(p, PAGE_IS_ROOT))		/* for all pages but root */
	if (!write_navigation(p, up, r)) /* write nav bar and title */
	    goto page_error;

    fputs(EOG_MARKER, p->out);		/* End Of Generated part */

    if (!body_process(p, up))
	goto page_error;

    FCLOSE(p->in)			/* finished with .h file */

    fputs(SOG_MARKER, p->out);		/* Start Of Generated trailer */

    if (p->style == PAGE_STYLE_BOOK)
    {
	fputs("  </td>\n", p->out);
	if (p->thumb)
	{
	    fprintf(p->out, "  <td width=%d>&nbsp;</td>\n", BOOK_SEPARATION);
	    fputs("  <td>\n", p->out);
	    if (!body_key_thumbn(p, up, p->thumb, 1))
		return FALSE;
	    fputs("  </td>\n", p->out);
	}
	fputs("</tr></table></center>\n", p->out);
    }

    fputs("  </body>\n</html>\n", p->out);

    /* if page introduces new rings, process them now */
    if (!single_page && p->n_rings)
    {
	if (debug > 2)
	    fprintf(stderr, "    n_rings = %d\n", p->n_rings);

	if (loading && (p != root))
	    fputs("<ul>\n", sitemap->out);
	for (i = 0; i < p->n_rings; i++)
	{
	    pr = p->rings[i];
	    if (!ring_process(pr, p))
		goto page_error;
	}
	if (loading && (p != root))
	    fputs("</ul>\n", sitemap->out);
    }

    if (debug > 2)
	fprintf(stderr, "<--   page_process p='%s', up='%s', r=%s\n",
		p->h, up ? up->h : "NULL",
		r ? r->path : "NULL");

    FCLOSE(p->out);
    level--;
    return TRUE;

page_error:
    level--;
    return FALSE;
}

/* ------------------------------ navigation ------------------------------ */

boolean homeup_navigation(page *p, page *up, ring *r)
{
    char  rp[MAX_LINE], *up_title;

    if (debug > 2)
	fprintf(stderr, "homeup_navigation(page=%s, up=%s, ring=%s)\n",
			p->h, up ? up->h : "NULL", r->path);

    /* relative path to root directory and its images subdir */
    rel_path(p->h, ROOT_HTML, rp);
    if (debug > 2)
	fprintf(stderr, "Rel path to ROOT_HTML: %s\n", rp);

    fputs("  <map name=um>\n", p->out);

    fprintf(p->out, "    <area href=\"%s\" shape=rect "
		    "coords=\"0,0,36,33\" alt=Home>\n", rp);

    rel_path(p->h, INDEX_HTML, rp);
    if (debug > 2)
	fprintf(stderr, "Rel path to INDEX_HTML: %s\n", rp);
    fprintf(p->out, "    <area href=\"%s\" shape=rect coords=\"37,28,63,33\" "
		    "alt=\"" INDEX_NAV "\" title=\"" INDEX_NAV "\">\n", rp);

    rel_path(p->h, SITEMAP_HTML, rp);
    if (debug > 2)
	fprintf(stderr, "Rel path to SITEMAP_HTML: %s\n", rp);
    fprintf(p->out, "    <area href=\"%s\" shape=rect coords=\"37,19,63,27\" "
		    "alt=\"" SITEMAP_NAV "\" title=\"" SITEMAP_NAV "\">\n", rp);

    if (up)
    {
	rel_path(p->h, up->html, rp);
	up_title = up->short_title;
    }
    else
    {
	strcpy(rp, "NO_LINK");
	up_title = "Unknown";
    }

    fprintf(p->out, "    <area href=\"%s\" shape=rect coords=\"37,0,63,18\" "
		    "alt=\"Up: %s\">\n", rp, up_title);

    fputs("  </map>\n", p->out);

    rel_path(p->h, IMAGES, rp);
    if (debug > 2)
	fprintf(stderr, "Rel path to IMAGES: %s\n", rp);
    fprintf(p->out, "  <img src=\"%s/%s\" width=%d height=%d "
		    "border=0 usemap=\"#um\" alt=Navigation><br>\n",
	    rp, ON(p, PAGE_INVERSE) ? INVNAV_GIF : NAV_GIF,
	    NAVGIF_WIDTH, NAVGIF_HEIGHT);

    fprintf(p->out, "  <font face=%s %ssize=%d>Up: %s</font>\n",
		GENSITE_FONT, ON(p, PAGE_INVERSE) ? "color=white " : "",
		NOTE_SIZE, up_title);

    return TRUE;
}

/* --------------------------------- body -------------------------------- */

/* copy input .h to output .html expanding @ tags.  two forms
   are allowed, a full-line version where @key starts the first:
	'@' key [add'l args separated by spaces] EOL
   and an inline version
	... '@@' key '{' [add'l args separated by spaces] '}' ... EOL
   the same output is produced, but the first consumes the EOL
   and the inline version does not.  note multiple inlines allowed.
*/
boolean body_process(page *p, page *up)
{
    char   line[MAX_LINE];
    int    line_no;

    if (debug > 2)
	fprintf(stderr, "-->   body_process(p='%s')\n", p->h);

    /* not watching for truncated lines */
    for (line_no = p->meta_lines; fgets(line, sizeof(line), p->in); line_no++)
	if (!line_process(p, up, line, line_no))
		return FALSE;
    return TRUE;
}

boolean line_process(page *p, page *up, char *line, int line_no)
{
    char   keybuf[MAX_FIELD];
    char  *rest, *key, *from, *e, *args, *start, *atat;
    int    len;

    /* check for raw hrefs */
    if (strstr(line, "href=") && !strstr(line, "<area") &&
				 !strstr(line, "function"))
    {
	printf("***** ERROR: page '%s' body line %d raw href\n",
		    p->h, line_no);
	return FALSE;
    }
    if (strstr(line, "helvetica"))
	printf("***** WARNING: page '%s' body line %d raw helvetica\n",
		    p->h, line_no);

    /* clobber ending NL */
    e = line + strlen(line) - 1;		/* last char */
    if (*e != '\n')
    {
	printf("***** ERROR: page '%s' body line %d overflow; max %d\n",
		    p->h, line_no, (int) sizeof(line));
	return FALSE;
    }
    *e = 0;					/* clobber NL */

    if (debug > 3)
	printf("    body line: '%s'\n", line);

    /* scan for @key at beginning or @@key anywhere else */
    if (*line == '@')
    {
	start = line + 1;
	if (*start == '@')
	    atat = line;
	else
	{
	    next_arg(start, keybuf, &args);
	    if (!do_body_key(p, up, keybuf, args))
	    {
		printf("***** ERROR: at line %d of %s (\"%s\")\n",
		    line_no, p->h, p->short_title);
		return FALSE;
	    }
	    /* continue; */ return TRUE;
	}
    }
    else
	atat = strstr(line, "@@");

    for (from = rest = line; atat; atat = strstr(from = rest, "@@"))
    {
	/* initial stuff before @key? */
	if ((len = atat - from))
	    fwrite(from, 1, len, p->out);

	for (key = atat + 2; *from != '{'; from++)
	    ;
	*from++ = 0;

	args = from;

	if (!(rest = strchr(from, '}')))
	{
	    printf("***** ERROR: missing close brace: "
		    "'%s' at line %d of %s\n", line, line_no, p->h);
	    return FALSE;
	}
	*rest++ = 0;

	if (!do_body_key(p, up, key, args))
	{
	    printf("***** ERROR: at line %d of %s (\"%s\")\n",
		    line_no, p->h, p->short_title);
	    return FALSE;
	}
    }

    if (rest)
	fputs(rest, p->out);	/* emit leftover */

    putc('\n', p->out);

    if (debug > 2)
	fprintf(stderr, "<--   body_process(p='%s')\n", p->h);

    return TRUE;
}

boolean do_body_key(page *p, page *up, char *key, char *args)
{
    int ki;

    /* search body keyword table for matching keyword */
    for (ki = 0; ki < N_KEYFUNCS; ki++)
	if (!strcmp(body_keyfuncs[ki].key, key))
	    break;

    if (ki == N_KEYFUNCS)
    {
	printf("***** ERROR: page '%s' unknown body key '%s'\n",
		    p->h, key);
	return FALSE;
    }

    if (debug > 3)
	fprintf(stderr, "=== @%s ===\n", key);

    if (!(*body_keyfuncs[ki].func)(p, up, args))
    {
	printf("***** ERROR: page '%s' @%s failed\n", p->h, key);
	return FALSE;
    }

    return TRUE;
}

/* ---------- RINGNAV ---------------------------------------
    @ringnav ring style size - Adds extra ring navigator
	    of same style as main page's navigator.

    ring - ring path (./ will be expanded)
    style - mask of STYLE bits seperated by | with no spaces
    size - font size, 0 for default
    cols - number of columns (if not specified, automatic)

    example: @ringnav ./paper.ring [style [size [cols]]]
*/

BODY_KEYFUNC(body_key_ringnav, p, up, s)
{
    char    *rest;
    ring    *r;
    char     rp[MAX_FIELD], rpath[MAX_PATH], buf[MAX_FIELD];
    unsigned ps;
    int      size, cols;

    if (!next_arg(s, rp, &rest))
	return FALSE;
    ps = RINGNAV_STYLE;
    size = RINGNAV_SIZE;
    cols = 0;
    if (rest)
    {
	if (!(ps = style_mask(next_arg(rest, buf, &rest))))
	    return FALSE;
	if (rest)
	{
	    if (!(size = atoi(next_arg(rest, buf, &rest))))
		size = RINGNAV_SIZE;
	    if (rest)
		cols = atoi(next_arg(rest, buf, &rest));
	}
    }

    rel_expand(rp, p->h, rpath);
    if (!(r = ring_register(p->h, rpath, p->theme, FALSE, FALSE)))
	return FALSE;

    return (ps & STYLE_NEXTPREV) ? ring_nextprev(p, r) :
	    ring_navigation(p, r, up, ps, size, cols,
			(ps & STYLE_SANS) ? GENSITE_SANS : NULL);
}

/* ---------- RINGHEAD --------------------------------------
    @ringhead ring style - Adds simple navigator to first page
	    in ring, with ring's name and size in a banner.

    ring - ring path (./ will be expanded)
    style - style keywords; only 'short' used (or leave blank)

    example: @ringhead ./elliott.ring short
*/

BODY_KEYFUNC(body_key_ringhead, p, up, args)
{
    char    *rest, *title, *style;
    list    *members;
    member  *m1;
    ring    *r;
    page    *mp;
    char     dest[MAX_FIELD], rp[MAX_PATH], rpath[MAX_PATH], argbuf[MAX_FIELD];
    unsigned m;
    int      size;

    if (!next_arg(args, rp, &rest))
	return FALSE;

    rel_expand(rp, p->h, rpath);
    if (!(r = ring_register(p->h, rpath, p->theme, FALSE, FALSE)))
	return FALSE;

    if (rest)
    {
	if (!(style = next_arg(rest, argbuf, &rest)))
	    return FALSE;
	m = style_mask(style);

	size = 0;
	if (rest)
	    size = atoi(next_arg(rest, argbuf, &rest));
	if (!size)
	    size = RINGHEAD_SIZE;
    }
    else
	m = 0;

    members = &r->members;
    m1 = (member *) LIST_HEAD(members);
    if (!(mp = m1->p))
	mp = ring_firstpage(m1->r);

    rel_path(p->h, mp->html, dest);

    title = (m & STYLE_SHORT) ? r->short_title : r->long_title;
    if (!write_link(dest, NULL, title, p))
	return FALSE;

    fprintf(p->out, " (%d)", (int) LIST_SIZE(members));
    return TRUE;
}

/* ---------- WEBRING ----------------------------------------
    @webring - Inserts [boilerplate] contents of webring file

    example: @webring
 */

BODY_KEYFUNC(body_key_webring, p, up, ignored_args)
{
    char  line[MAX_LINE];
    FILE *f;

    if (!(f = fopen(WEBRING, "r")))
    {
	printf("***** ERROR: failed to open webring file '%s'\n", WEBRING);
	return FALSE;
    }

    fputs("<!-- BEGIN WEBRING -->\n", p->out);

    /* not watching for truncated lines */
    while (fgets(line, sizeof(line), f))
	fputs(line, p->out);

    fclose(f);

    fputs("<!-- END WEBRING -->\n", p->out);

    return TRUE;
}

/* ---------- INDEX ------------------------------------------
    @index <letter> <entry> [ '->' <see also> ] - Adds index entry
    @index-main <letter> <entry> [ '->' <see also> ] - Adds main index entry
    @index-image <letter> <entry> [ '->' <see also> ] - Adds image index entry

    example: @index A Arrdvarks
 */

BODY_KEYFUNC(body_key_index, p, up, args)
{
    return body_key_index_worker(p, up, args, FALSE, FALSE, FALSE);
}

BODY_KEYFUNC(body_key_index_main, p, up, args)
{
    return body_key_index_worker(p, up, args, TRUE, FALSE, FALSE);
}

BODY_KEYFUNC(body_key_index_image, p, up, args)
{
    return body_key_index_worker(p, up, args, FALSE, TRUE, FALSE);
}

void make_sort_key(char *raw, char *dest)
{
    struct INDEX_ENTITY *e;
    char *to, *from, stripped[MAX_FIELD];
    int c;

    strip_tags(raw, stripped);		/* strip HTML tags */
    lowercaseify(stripped);		/* force to all lowercase */

    /* reduce html entities and special case "st." -> "saint" */
    for (from = stripped, to = dest; ((c = *from)); from++)
    {
	e = NULL;
	if (c == '&')
	{
	    for (e = &index_entities[0]; e->entity; e++)
	    {
		if (!strncmp(from + 1, e->entity, e->entlen))
		{
		    strcpy(to, e->ascii);
		    to += strlen(e->ascii);
		    from += e->entlen;
		    break;
		}
	    }
	}
	else if (!strncmp(from, "st.", 3))
	{
	    strcpy(to, "saint");
	    to += 5;
	    from += 3;
	    continue;
	}
	if (!e && !strchr(INDEX_KEYSTRIP, c))
	    *to++ = c;
    }
    *to = 0;
}

boolean body_key_index_worker(page *p, page *up, char *args,
			      boolean main, boolean image, boolean top)
{
    char   argbuf[MAX_FIELD];
    char *to, *from, tag[MAX_FIELD], fulltag[MAX_FIELD];
    char   sort[MAX_FIELD];	/* entry text stripped for sorting */
    char  *rest, *see, *letter, *op, *cp;
    entry *e, *f;		/* index entries */
    list  *ll;			/* entries list for letter */
    int    l, l0;		/* index letter 'A'; zero-based, 'A'-> 0 */
    int    s;			/* sort order (strcmp retval) */
    int    c, n, skip;

    if (!(letter = next_arg(args, argbuf, &rest)))
	return FALSE;
    if (strlen(letter) != 1)
    {
	printf("***** ERROR: index letter '%s' wrong length (not 1)\n", letter);
	return FALSE;
    }
    l = toupper(*letter);
    if ((l < 'A') || (l > 'Z'))
    {
	printf("***** ERROR: index letter '%c' not A-Z\n", l);
	return FALSE;
    }
    l0 = l - 'A';			/* turn into 0-based index */

    if ((see = strstr(rest, "->")))
    {
	*see = 0;
	if (*(see - 1) == ' ')
	   *(see - 1) = 0;
	see += 2;
	if (*see == ' ')
	    see++;
    }

    CALLOC(e, entry)
    e->page = p;			/* index entry points to this page */
    e->text = save_string(rest);	/* full text of entry */

    e->main = main;			/* main entry? */
    e->image = image;			/* image entry? */

    /* form tag from initial letters/digits */
    to = tag;
    if (!top)
    {
	*to++ = l;			/* start w/index letter */
	for (from = rest; ((c = *from++)); )
	{
strip:
	    switch (c)				/* something to skip over? */
	    {
		case '<': skip = '>'; break;	/* tag? */
		case '(': skip = ')'; break;	/* parenthetical? */
		default:  skip = 0;
	    }
	    if (skip)
	    {
		while (((c = *from)) && (c != skip))
		    from++;			/* skip it */
		if (!(c = *++from))		/* following char */
		    break;
		goto strip;
	    }
	    if (isalnum(c))
	    {
		*to++ = toupper(c);
		while ((c = *from))
		{
		    if (c == '&')
		    {
			while (((c = *++from)) && (c != ';'))
			    ;
			c = *++from;
		    }
		    if (!isalnum(c))
			break;
		    from++;			/* skip rest of word/num */
		}
	    }
	    if (!c)
		break;
	}
    }
    *to = 0;

    /* if already exists, try again after appending 2, 3, 4... */
    if (*tag)
    {
	sprintf(fulltag, "%s#%s", p->html, tag);
	if (hash_lookup(tags, fulltag))		/* already exists? */
	{
	    sprintf(fulltag, "%s#%s%d", p->html, tag, n = 2);
	    while (hash_lookup(tags, fulltag))
		sprintf(fulltag, "%s#%s%d", p->html, tag, ++n);
	    sprintf(to, "%d", n);
	}
	e->tag = save_string(tag);		/* #tag */
	hash_add(tags, save_string(fulltag), p);
    }

    /* split "primary|secondary (comment)" */
    cp = rest + strlen(rest) - 1;
    if (*cp == ')')
    {
	for (op = cp - 1; (op > rest) && (*op != '('); op--) ;
	if (op == rest)
	{
	    puts("***** ERROR: Bungled parens in index");
	    return FALSE;
	}
	*(op - 1) = *cp = 0;
	e->comment = save_string(op + 1);
    }
    if ((op = strchr(rest, '|')))
    {
	*op = 0;
	e->secondary = save_string(op + 1);
    }
    e->primary = save_string(rest);

    make_sort_key(e->primary, sort);
    e->primary_sort = save_string(sort);/* we'll sort using this key */

    if (e->secondary)
    {
	make_sort_key(e->secondary, sort);
	e->secondary_sort = save_string(sort);
    }

    if (see)
	e->see = save_string(see);
    ll = &entries[l0];			/* list of entries for letter l */
    if (!LIST_SIZE(ll))			/* this is first entry? */
	LINK_HEAD(ll, e)		/* yes, start list. */
    else				/* else add in sorted order */
    {	/* scan for 'larger' string, keep list in increasing order */
	for (f = (entry *) LIST_HEAD(ll); f; f = LINK_NEXT(f))
	{
	    if (!(s = strcmp(f->primary_sort, e->primary_sort)))
	    {
		if (e->secondary)
		{
		    if (f->secondary)
			s = strcmp(f->secondary_sort, e->secondary_sort);
		    else
			s = 0;
		}
		else
		    s = f->secondary ? 1 : 0;
	    }
	    if (s > 0)
		break;			/* went too far, so... */
	}
	if (f)				/* new entry comes right before f */
	    LINK_BEFORE(ll, f, e)
	else
	    LINK_TAIL(ll, e)		/* new entry comes last */
    }

    if (e->tag)
	fprintf(p->out, "<a name=%s></a>", e->tag);

    n_index++;				/* bump total # index entries */
    return TRUE;
}

/* ---------- AREA --------------------------------------------
    @area <image> [img tags] <alt> - Includes local image <image>.
	optional img tags (identified by '=' in the middle) are
	followed by required alt text.

    example: @area ./foo.jpg hspace=4 Authentic Foo ca1960s
 */

BODY_KEYFUNC(body_key_area, p, up, args)
{
    char *image, *rest, argbuf[MAX_FIELD];

    if (!(image = next_arg(args, argbuf, &rest)))
	return FALSE;
    if (!image_register(p->h, image)) /* get image metadata, write manifest */
	return FALSE;
    fprintf(p->out, AREA_FORMAT, image, rest);
    return TRUE;
}

/* ---------- CELL BACKGROUND -------------------------------------------
    @cellbg <image> - sets background image for cell

    example: @cellbg ./foo.jpg
 */

BODY_KEYFUNC(body_key_cellbg, p, up, args)
{
    char *impath, *rest, argbuf[MAX_FIELD], rp[MAX_PATH];
    image *im;

    if (!(impath = next_arg(args, argbuf, &rest)))
	return FALSE;
    if (!(im = image_register(p->h, impath)))
        return FALSE;
    rel_path(p->h, im->path, rp);
    fprintf(p->out, "background=\"%s\"", rp);
    return TRUE;
}

/* ---------- IMAGE -------------------------------------------
    @image <image> [img tags] <alt> - Includes local image <image>.
	optional img tags (identified by '=' in the middle) are
	followed by required alt text.

    example: @image ./foo.jpg hspace=4 Authentic Foo ca1960s
 */

BODY_KEYFUNC(body_key_image, p, up, args)
{
    char *image, *rest, argbuf[MAX_FIELD];

    if (!(image = next_arg(args, argbuf, &rest)))
	return FALSE;
    return image_write(p, image, NULL, NULL, rest, 0);
}

/* ---------- IMAGE-LINK --------------------------------------
    @image-link <image> <text> - Makes a hypertext link to an image.

    example: @image-link ./foo.jpg Picture of a Foo
 */

BODY_KEYFUNC(body_key_image_link, p, up, args)
{
    char  *path, *rest;
    char   rp[MAX_PATH], argbuf[MAX_FIELD];
    image *im;

    if (!(path = next_arg(args, argbuf, &rest)))
	return FALSE;
    if (!(im = image_register(p->h, path)))
	return FALSE;
    rel_path(p->h, im->path, rp);
    return write_link(rp, NULL, rest, p);
}

/* ---------- IMAGE-PAGE --------------------------------------
    @image-page <image> <page> [img tags] <alt> - Makes an image
	hypertext linked to a page.

    example: @image-page ./foo.jpg bar.h Picture of a Foo
 */
BODY_KEYFUNC(body_key_image_page, p, up, args)
{
    char *rest, tag[MAX_FIELD], dest[MAX_FIELD], image[MAX_FIELD];
    page *to;

    if (!next_arg(args, image, &rest))
	return FALSE;
    if (!image_register(p->h, image))
	return FALSE;
    if (!next_arg(rest, dest, &rest))
	return FALSE;
    if (!(to = page_register(p->h, dest, up ? up->theme : NULL,
				FALSE, FALSE, tag)))
	return FALSE;
    return image_write(p, image, to->html, tag, rest, 0);
}

/* ---------- THUMB-IMAGE -------------------------------------
    @thumb-image <thumb> <image> [img tags] <alt> - Includes a
        thumbnail-image link <image> which links to full-size
	<image>.

    example: @thumb-image ./foo.jpg Top/bigfoo.jpg border=0 Major Foo Action
 */

BODY_KEYFUNC(body_key_thumb_image, p, up, args)
{
    char *thumb, *image, *rest, thumbuf[MAX_FIELD], imgbuf[MAX_FIELD];

    if (!(thumb = next_arg(args, thumbuf, &rest)))
	return FALSE;
    if (!(image = next_arg(rest, imgbuf, &rest)))
	return FALSE;
    return image_write(p, thumbuf, imgbuf, NULL, rest, 0);
}

/* ---------- THUMB-LINK -------------------------------------
    @thumb-link <thumb> <thing> [img tags] <alt> - Links a
        thumbnail image to any linkable thing.

    example: @thumb-link ./foo.jpg Top/foo.pdf border=0 Foo Now Foo Often
 */

BODY_KEYFUNC(body_key_thumb_link, p, up, args)
{
    char *thumb, *dest, *rest;
    char thumbuf[MAX_FIELD], destbuf[MAX_FIELD], rp[MAX_PATH];

    if (!(thumb = next_arg(args, thumbuf, &rest)))
	return FALSE;

    if (!(dest = next_arg(rest, destbuf, &rest)))
	return FALSE;

    if (!(dest = dest_parse(dest, rp, p, up, FALSE)))
	return FALSE;

    return image_write(p, thumb, dest, NULL, rest, 0);
}

/* ---------- THUMB -------------------------------------------
    @thumb <image> [img tags] <alt> - Includes thumbnail of image
	<image> linked to full-size image; path may be ./relative.
	optional img tags (identified by '=' in the middle) are
	followed by required alt text.  name of thumbname is formed
	by <rootname>t.<extension>, e.g. "bar.jpg" has a thumbnail
	named "bart.jpg".

    example: @thumb ./foo.jpg hspace=4 vspace=4 Authentic Foo ca1960s
 */

BODY_KEYFUNC(body_key_thumb, p, up, args)
{
    return body_key_thumbn(p, up, args, 1);
}

BODY_KEYFUNC(body_key_tthumb, p, up, args)
{
    return body_key_thumbn(p, up, args, 2);
}

boolean body_key_thumbn(page *p, page *up, char *args, int n_t)
{
    char  thumb[MAX_PATH], argbuf[MAX_FIELD];
    char *image, *rest, *e;
    int   len, ext_len;

    if (!(image = next_arg(args, argbuf, &rest)))
	return FALSE;
    len = strlen(image);
    strcpy(thumb, image);
    e = thumb + len - 1;		/* point to last char */
    for (ext_len = 0; (e >= thumb) && (*e != '.'); ext_len++)
	e--;				/* back up to '.' */
    if (n_t < 0)
    {
	*e++ = '.';
	*e++ = '1';
    }
    else
	while (n_t-- > 0)		/* add <n_t> t's to the name */
	    *e++ = 't';			/* "foo.jpg" -> "footjpg" */
    *e++ = '.';				/* foot.pg" */
    strcpy(e, image + len - ext_len);	/* foot.jpg" */
    return image_write(p, thumb, image, NULL, rest, n_t);
}

/* ---------- XLINK -------------------------------------------
    @xlink <dest> <text> - Creates absolute link to external page
			   with given hypertext <text>

    example: @xlink http://benevolent.org/permanent.resource Benevolent
 */

BODY_KEYFUNC(body_key_xlink, p, up, args)
{
    char *dest, *rest, params[MAX_FIELD], argbuf[MAX_FIELD];

    if (!(dest = next_arg(args, argbuf, &rest)))
	return FALSE;
    if (!rest || !*rest)		/* no text? */
	return FALSE;
    if (xlinks)
    {
	fputs(p->h, xlinks);		/* page with the link */
	putc(' ', xlinks);
	fputs(dest, xlinks);		/* write to list of all xlinks */
	putc('\n', xlinks);
    }
    rest = get_params(rest, params);
    return write_link(dest, params, rest, p);
}

/* ---------- XLINK-IMAGE -------------------------------------
    @xlink-image <dest> <image> [img tags] <alt> - Creates absolute link to
	destination page with given hypertext <image>.  zero or more
	img tags are allowed, identified by '=' sign.  An <alt> is required.

    example: @xlink-image http://www.sediver.fr/ ./logo.gif border=0 Sediver
 */

BODY_KEYFUNC(body_key_xlink_image, p, up, args)
{
    char *dest, *rest, *image, destbuf[MAX_FIELD], imgbuf[MAX_FIELD];

    if (!(dest = next_arg(args, destbuf, &rest)))
	return FALSE;
    if (!(image = next_arg(rest, imgbuf, &rest)))
	return FALSE;
    if (!rest || !*rest)		/* no text? */
	return FALSE;
    if (xlinks)
    {
	fputs(p->h, xlinks);
	putc(' ', xlinks);
	fputs(dest, xlinks);		/* write to list of all xlinks */
	putc('\n', xlinks);
    }
    return image_write(p, image, dest, NULL, rest, 0);
}

/* ---------- LIBROW ------------------------------------------
    @librow <id> [<id2>...] - produce row of library entries
    example: @librow{gedde, mark1884, champ1884}
 */

BODY_KEYFUNC(body_key_lib_row, p, up, args)
{
    char *id, *rest;
    char argbuf[MAX_FIELD], path[MAX_FIELD], *ids[MAX_LIB_ROW];
    FILE *fn[MAX_LIB_ROW];
    unsigned n, i;

    if (debug > 2)
	fprintf(stderr, "body_key_lib_row p='%s', args='%s'\n", p->h, args);

    if (!(id = next_arg(args, argbuf, &rest)))
    {
	puts("***** ERROR: @librow no id");
	return FALSE;
    }

    fputs("<table summary=Library>\n", p->out);
    fputs("    <tr align=center valign=bottom>\n", p->out);

    ids[0] = save_string(id);

    sprintf(path, "Library/%s", id);
    if (!(fn[0] = fopen(path, "r")))
    {
	printf("***** ERROR: lib entry %s missing\n", path);
	return FALSE;
    }

    if (!proc_lib(p, up, id, fn[0], TRUE))
	return FALSE;

    for (n = 1; ((id = next_arg(args = rest, argbuf, &rest))); n++)
    {
	if (n >= MAX_LIB_ROW - 1)
	{
	    puts("***** ERROR: too many librow entries");
	    return FALSE;
	}
	ids[n] = save_string(id);

	sprintf(path, "Library/%s", id);
	if (!(fn[n] = fopen(path, "r")))
	{
	    printf("***** ERROR: lib entry %s missing\n", path);
	    return FALSE;
	}

	fputs("\n\t<td width=20 rowspan=2/>\n\n", p->out);

	if (!proc_lib(p, up, id, fn[n], TRUE))
	    return FALSE;
    }

    fputs("    </tr>\n", p->out);
    fputs("\n    <tr align=center valign=top>\n",  p->out);

    for (i = 0; i < n; i++)
    {
	if (i)
	    fputc('\n', p->out);
	if (!proc_lib(p, up, ids[i], fn[i], FALSE))
	    return FALSE;
    }

    fputs("    </tr>\n</table>\n\n", p->out);

    return TRUE;
}

boolean proc_lib(page *p, page *up, char *id, FILE *f, boolean first)
{
    char line[MAX_LINE];
    boolean ok;
    int line_no;
    FILE *in;

    if (!id || !*id)
	return FALSE;

    fputs("\t<td>", p->out);
    if (first && ON(p, PAGE_IS_LIBRARY))
	fprintf(p->out, "<a id=\"%s\"></a>\n", id);

    in = p->in;
    p->in = f;
    for (line_no = 1; fgets(line, sizeof(line), f); line_no++)
    {
	if (first && (*line == '\n'))	/* first part ends with blank line */
	    break;
	if (ON(p, PAGE_IS_LIBRARY) || strncmp(line, "@index", 6))
	    if (!(ok = line_process(p, up, line, line_no)))
		return FALSE;
    }

    fputs("\t</td>\n", p->out);
    if (!first)
	fclose(f);
    p->in = in;
    return ok;
}

/* ---------- LINK ------------------------------------------
    @link <dest> <text> - Creates relative link to destination page
			  with given hypertext <text>

    example: See the @@link{biblio.h,Bibliography}
 */

BODY_KEYFUNC(body_key_link, p, up, args)
{
    char *dest, *rest;
    char params[MAX_FIELD], argbuf[MAX_FIELD], rp[MAX_PATH];

    if (debug > 2)
	fprintf(stderr, "body_key_link p='%s', args='%s'\n", p->h, args);
    if (!(dest = next_arg(args, argbuf, &rest)))
    {
	puts("***** ERROR: @link missing destination");
	return FALSE;
    }
    if (debug > 2)
	fprintf(stderr, "  dest='%s', rest='%s'\n", dest, rest);

    if (!rest || !*rest)		/* no text? */
    {
	puts("***** ERROR: @link missing text");
	return FALSE;
    }

    if (!(dest = dest_parse(dest, rp, p, up, TRUE)))
	return FALSE;

    rest = get_params(rest, params);
    return write_link(dest, params, rest, p);
}

/* parse a destination */

char *dest_parse(char *dest, char *rp, page *p, page *up, int relp)
{
    char tag[MAX_FIELD], path[MAX_PATH];
    ring *r;
    page *to;
    struct stat s;

    *rp = 0;

    /* if linking to an image, figure the image's metadata */
    if (has_extension(dest, "gif") || has_extension(dest, "jpg") ||
	has_extension(dest, "png") || has_extension(dest, "tif"))
    {
	rel_expand(dest, p->h, path);
	if (!image_register(p->h, path))
	    return NULL;
	if (relp)
	    rel_path(p->h, path, rp);
	else
	    strcpy(rp, path);
	dest = rp;
    }
    else if (has_extension(dest, "ring"))
    {
	if (!(r = ring_register(p->h, dest, p->theme, FALSE, FALSE)))
	    return NULL;
	to = ring_firstpage(r);
	if (relp)
	    rel_path(p->h, to->html, rp);
	else
	    strcpy(rp, to->html);
	dest = rp;
    }
    else if (has_extension(dest, "html"))
    {
	if (p != index_page)
	    printf("*** Warning: @link to raw HTML '%s'\n", dest);
	rel_expand(dest, p->h, path);
	if (relp)
	    rel_path(p->h, path, rp);
	else
	    strcpy(rp, path);
	dest = rp;

	if (!strchr(path, '#'))		/* not index links */
	    add_to_manifest(path);
    }
    else if (has_extension(dest, "h"))
    {
	if (!(to = page_register(p->h, dest, up ? up->theme : NULL,
					FALSE, FALSE, tag)))
	    return NULL;
	if (relp)
	    rel_path(p->h, to->html, rp);
	else
	    strcpy(rp, to->html);
	dest = rp;
	if (*tag)
	{
	    strcat(dest, "#");
	    strcat(dest, tag);
	}
    }
    else if (has_extension(dest, "c") || has_extension(dest, "pdf") ||
	     has_extension(dest, "tar") || has_extension(dest, "zip"))
    {
	rel_expand(dest, p->h, path);

	if (stat(path, &s))
	{
	    printf("***** ERROR: linked-to file %s does not exist\n", path);
	    return NULL;
	}
	if (relp)
	    rel_path(p->h, path, rp);
	else
	    strcpy(rp, path);
	dest = rp;

	add_to_manifest(path);
    }
    else if (has_extension(dest, "H"))
    {
	*(dest + strlen(dest) - 1) = 'h';
	if (stat(dest, &s))
	{
	    printf("***** ERROR: linked-to header %s does not exist\n", dest);
	    return NULL;
	}
	if (relp)
	    rel_path(p->h, dest, rp);
	else
	    strcpy(rp, dest);
	dest = rp;
    }
    else if (*dest != '#')
    {
	printf("***** ERROR: linked-to file %s not a .{ring,c,H,h,gif,jpg,pdf,tar}\n",
		dest);
	return NULL;
    }

    return *rp ? rp : dest;
}

/* ---------- CD ----------------------------------------------
    @cd <cd number> - Create CD reference page

    example: @cd 102
 */

BODY_KEYFUNC(body_key_cd, p, up, args)
{
    cd_data cdd;
    char   *cd, *rest, argbuf[MAX_FIELD];

    if (!(cd = next_arg(args, argbuf, &rest)))
	return FALSE;
    if (!load_cd_data(cd, &cdd))
	return FALSE;
    fputs("<table cellpadding=0 cellspacing=0>\n", p->out);
    fprintf(p->out, CD_FORMAT, "Width",   cdd.width);
    fprintf(p->out, CD_FORMAT, "Height",  cdd.height);
    fprintf(p->out, CD_FORMAT, "Weight",  cdd.weight);
    fprintf(p->out, CD_FORMAT, "Voltage", cdd.voltage);
    fprintf(p->out, CD_FORMAT, "Leakage", cdd.leakage);
    fprintf(p->out, CD_FORMAT, "Pinhole", cdd.pinhole);
    fprintf(p->out, CD_FORMAT, "Style#",  cdd.style);
    fputs("</table>\n", p->out);
    return TRUE;
}

/* ---------- CDCMP -------------------------------------------
    @cdcmp <cd 1> <cd 2> - Create CD comparison page

    example: @cdcmp 161 162
 */

BODY_KEYFUNC(body_key_cdcmp, p, up, args)
{
    char   *cd1, *cd2, *rest, argbuf[MAX_FIELD];
    cd_data cdd1, cdd2;

    if (!(cd1 = next_arg(args, argbuf, &rest)))
	return FALSE;
    if (!load_cd_data(cd1, &cdd1))
	return FALSE;

    if (!(cd2 = next_arg(rest, argbuf, &rest)))
	return FALSE;
    if (!load_cd_data(cd2, &cdd2))
	return FALSE;

    fprintf(p->out, "<table cellpadding=0 cellspacing=2 "
		"summary=\"Comparison of CD %s and %s\"><tr>\n", cd1, cd2);

    fputs("<th></th>\n", p->out);

    fprintf(p->out, "<th><font face=%s size=%s color=blue>"
		    "<b>CD %s</b></font></th>", GENSITE_FONT, CDCMP_SIZE, cd1);
    fputs("<td></td>", p->out);
    fprintf(p->out, "<th><font face=%s size=%s color=red><b>CD %s</b>"
		    "</font></th>\n", GENSITE_FONT, CDCMP_SIZE, cd2);
    fputs("</tr>\n", p->out);

#define CDCMP_OUT(p, label, thing1, thing2) \
    fprintf(p->out, CDCMP_FORMAT, GENSITE_FONT, CDCMP_SIZE, (label), \
				  GENSITE_FONT, CDCMP_SIZE, (thing1), \
				  GENSITE_FONT, CDCMP_SIZE, (thing2))

    CDCMP_OUT(p, "Width",   cdd1.width,   cdd2.width);
    CDCMP_OUT(p, "Height",  cdd1.height,  cdd2.height);
    CDCMP_OUT(p, "Weight",  cdd1.weight,  cdd2.weight);
    CDCMP_OUT(p, "Voltage", cdd1.voltage, cdd2.voltage);
    CDCMP_OUT(p, "Leakage", cdd1.leakage, cdd2.leakage);
    CDCMP_OUT(p, "Pinhole", cdd1.pinhole, cdd2.pinhole);
    CDCMP_OUT(p, "Style#",  cdd1.style,   cdd2.style);

    fputs("</table>\n", p->out);
    return TRUE;
}

#define CD_FIELD(field) \
    if (!(bar = strchr(from, '|'))) \
	{ printf("***** ERROR: bad CD %s data file '%s'\n", cd, path); \
	  return FALSE; } \
    *bar = 0; \
    if ((len = bar - from) >= MAX_SHORT_FIELD) { \
	printf("***** ERROR: CD %s data file field overflow\n", cd); \
	return FALSE; } \
    strcpy(cdd->field, from); \
    from = bar + 1;

boolean load_cd_data(char *cd, cd_data *cdd)
{
    char  path[MAX_PATH], line[MAX_LINE];
    char *from, *bar;
    FILE *f;
    int   len;

    sprintf(path, CD_DATA_FORMAT, cd, cd);
    if (!(f = fopen(path, "r")))
    {
	printf("***** ERROR: missing CD data file '%s'\n", path);
	return FALSE;
    }

    if (!(fgets(line, sizeof(line), f)))
    {
	printf("***** ERROR: empty CD data file '%s'\n", path);
	return FALSE;
    }

    from = line;

    memset(cdd, 0, sizeof(cd_data));
    CD_FIELD(width)
    CD_FIELD(height)
    CD_FIELD(weight)
    CD_FIELD(voltage)
    CD_FIELD(leakage)
    CD_FIELD(pinhole)

    if (strlen(from) >= MAX_SHORT_FIELD)
    {
	puts("***** ERROR: CD data file field overflow (style)");
	return FALSE;
    }
    strcpy(cdd->style, from);

    fclose(f);
    return TRUE;
}

/* ---------- FONT -------------------------------------------
    @font - insert main font name

    example: <font face=@@font{}>
 */

BODY_KEYFUNC(body_key_font, p, up, args)
{
    fputs(GENSITE_FONT, p->out);
    return TRUE;
}

/* ---------- FIXED -------------------------------------------
    @font - insert fixed-width font name

    example: <font face=@@fixed{}>
 */

BODY_KEYFUNC(body_key_fixed, p, up, args)
{
    fputs(GENSITE_FIXED, p->out);
    return TRUE;
}

/* ---------- SANS -------------------------------------------
    @sans - insert sans-serif font name

    example: <font face=@@sans{}>
 */

BODY_KEYFUNC(body_key_sans, p, up, args)
{
    fputs(GENSITE_SANS, p->out);
    return TRUE;
}

/* ---------- WIDTH -------------------------------------------
    @width - insert primary page width

    example: <table width=@@width{}>
 */

BODY_KEYFUNC(body_key_width, p, up, args)
{
    fprintf(p->out, "%s", GENSITE_WIDTH);
    return TRUE;
}

/* --------------------------------- html -------------------------------- */

boolean write_link(char *link_href, char *params, char *link_text, page *p)
{
    if (!link_text || !*link_text)
    {
	printf("***** ERROR: empty link text for '%s'\n", link_href);
	return FALSE;
    }
    fprintf(p->out, "<a href=\"%s\"", link_href);
    if (params && *params)
	fprintf(p->out, " %s", params);
    fprintf(p->out, ">%s</a>", link_text);
    return TRUE;
}

void write_meta(page *p, ring *r)
{
    char  title[MAX_FIELD], back[MAX_FIELD];
    char *desc, *keys, *bg;

    /* note first write uses ">" and creates the output.
       all subsequent writes must be >> to append
    */
    fprintf(p->out, "%s\n<html lang=\"%s\">\n<head>\n", DOCTYPE, LANGUAGE);
    strip_tags(p->long_title, title);
    fprintf(p->out, "  <title>%s", title);
    if (r)
	fprintf(p->out, " | %s", GENSITE_NAME);
    fputs("</title>\n", p->out);
    fprintf(p->out, "  <meta http-equiv=\"Content-Type\" "
		    "content=\"%s; charset=%s\">\n",
		CONTENT_TYPE, CHARSET);
    fputs("  <meta name=viewport content=\"width=device-width, initial-scale=1\">\n", p->out);
    if ((desc = p->description))
	fprintf(p->out, "  <meta name=description content=\"%s\">\n", desc);
    if ((keys = p->keywords))
	fprintf(p->out, "  <meta name=keywords content=\"%s\">\n", keys);

    if (p->css)
	fprintf(p->out, "  <style type=\"text/css\">\n    %s\n  </style>\n",
		p->css);

    fputs("</head>\n", p->out);

    if (p->background)
	sprintf(back, "background=%s", p->background);
    else
    {
	bg = NULL;
	if (ON(p, PAGE_INVERSE))
	    bg = INVERSE_BODY_COLOR;
	else if (!(bg = p->bg))
	{
	    if (r == root_ring)
		bg = BODY_COLOR;
	    else if (p->style == PAGE_STYLE_BOOK)
		bg = r->bookbg;
	    if (!bg)
		bg = BODY_COLOR;
	    p->bg = bg;
	}
	sprintf(back, "bgcolor=%s", bg);
    }

    fprintf(p->out, BODY_FORMAT, back, TEXT_COLOR, LINK_COLOR, VLINK_COLOR);
}

boolean write_navigation(page *p, page *up, ring *r)
{
    char *theme, *bookpage, *font_color, *q, *t;
    char  pathbuf[MAX_LINE];
    int   len, ri, hs, c, i;

    if (debug > 2)
	fprintf(stderr, "-->   write_navigation(page=%s, up=%s, ring=%s)\n",
		p->h, up ? up->h : "NULL", r->path);

    switch (p->style)
    {
	case PAGE_STYLE_DEFAULT:

	    fprintf(p->out, "  <center><table width=%s "
			    "summary=\"Title and Navigation\"><tr>\n",
			GENSITE_WIDTH);
	    fputs("  <td align=center width=\"15%\">\n", p->out);
	    if (!homeup_navigation(p, up, r))
		return FALSE;
	    fputs("  </td>\n", p->out);

	    if (!(theme = p->theme))
		if (!(theme = r->theme))
		    theme = THEME_COLOR;
	    fprintf(p->out, "  <td align=center bgcolor=%s>\n", theme);

	    for (len = i = 0, t = p->long_title; ((c = t[i++])); len++)
		if (c == '&')
		    while (t[i] && (t[i] != ';'))
			i++;

	    if (len > 75)      hs = HEADING_SIZE - 1;
	    else if (len > 55) hs = HEADING_SIZE;
	    else if (len > 40) hs = HEADING_SIZE + 1;
	    else if (len > 25) hs = HEADING_SIZE + 2;
	    else               hs = HEADING_SIZE + 3;

	    if (p == sitemap)
	    {
		fprintf(p->out, "  <b><font face=%s size=%d>"
				SITEMAP_LONG "<br><font size=%d>"
				"%d pages and %d images in "
				"%d rings</font></font></b>\n",
			GENSITE_FONT, hs, NOTE_SIZE,
			n_pages + 1, n_images, n_rings);
	    }
	    else if (p == index_page)
	    {
		fprintf(p->out, "  <b><font face=%s size=%d>"
				INDEX_LONG "<br><font size=%d>"
				"%d entries</font></font></b>\n",
			GENSITE_FONT, hs, NOTE_SIZE, n_index);
	    }
	    else
	    {
		fprintf(p->out, "  <font face=%s size=%d", GENSITE_FONT, hs);

		/* for inverse color scheme, or when the theme is dark, we
		   use white text
		*/
		font_color = NULL;
		if (ON(p, PAGE_INVERSE))
		    font_color = INVERSE_THEME_COLOR;
		else
		{
		    if ((q = strchr(theme, '#')))
		    {
			unsigned r, g, b;

			if (sscanf(q + 1, "%2X%2X%2X", &r, &g, &b) != 3)
			{
			    printf("***** ERROR: Problem with RGB /%s/\n",
					theme);
			    return FALSE;
			}
			else
			{
			    float luminosity =	(float) r / 255.0 * 0.30 +
						(float) g / 255.0 * 0.59 +
						(float) b / 255.0 * 0.11;
			    if (luminosity < 0.5)
				font_color = "white";
			}
		    }
		    else
		    {
			puts("***** ERROR: Can't handle color name here yet!");
			return FALSE;
		    }
		}
		if (font_color)
		    fprintf(p->out, " color=%s", font_color);

		fprintf(p->out, "><b>%s</b>", p->long_title);
		*pathbuf = 0;
		page_path(p, p, pathbuf, 0);
		fprintf(p->out, "<br><font size=\"-2\">%s</font>", pathbuf);
		fputs("</font>\n", p->out);
	    }
	    fputs("</td>\n", p->out);

	    fputs("  <td align=center width=\"15%\">\n", p->out);
	    if (!ring_nextprev(p, r))
		return FALSE;
	    fputs("  </td></tr></table></center><br>\n", p->out);
	    break;

	case PAGE_STYLE_BOOK:

	    fputs("  <center><table cellspacing=4><tr>\n", p->out);
	    fputs("<td valign=top><center>\n", p->out);
	    if (!homeup_navigation(p, up, r))
		return FALSE;
	    fputs("<br><br>\n", p->out);

	    if (!ring_selector(p, p->ring, up, p->ringnav_size,
				p->ringnav_cols))
		return FALSE;

	    /* page wants addl ring navigators? */
	    if (p->n_ringnavs)
	    {
		for (ri = 0; ri < p->n_ringnavs; ri++)
		{
		    fputs("<br>\n", p->out);
		    if (!ring_selector(p, p->ringnavs[ri],
				       up, p->ringnavsize[ri], 0))
			return FALSE;
		}
	    }

	    /* left page thumb? */
	    if (p->thumb_left)
	    {
		fputs("<p>", p->out);
		if (!body_key_thumbn(p, up, p->thumb_left, 1))
		    return FALSE;
		fputs("</p>\n", p->out);

		if (p->thumb_left2)
		{
		    fputs("<p>", p->out);
		    if (!body_key_thumbn(p, up, p->thumb_left2, 1))
			return FALSE;
		    fputs("</p>\n", p->out);
		}
	    }
	    else if (p->tthumb_left)
	    {
		fputs("<p>", p->out);
		if (!body_key_thumbn(p, up, p->tthumb_left, 2))
		    return FALSE;
		fputs("</p>\n", p->out);
	    }

	    if (p->thumb_11)
	    {
		fputs("<p>", p->out);
		if (!body_key_thumbn(p, up, p->thumb_11, -1))
		    return FALSE;
		fputs("</p>\n", p->out);
	    }

	    fputs("  </center>\n", p->out);

	    if (!(bookpage = p->bookpage))
		if (!(bookpage = r->bookpage))
		    bookpage = BODY_COLOR;

	    fprintf(p->out, "</td><td width=%d>&nbsp;</td>"
			    "<td bgcolor=%s width=%s height=%s>\n",
		    BOOK_SEPARATION, bookpage, p->width, p->height);
	    break;

	default:
	    /* other styles do not get nevigation bar */
	    break;
	
    }

    if (debug > 2)
	fprintf(stderr, "<--   write_navigation(page=%s, up=%s, ring=%s)\n",
		p->h, up ? up->h : "NULL", r->path);

    return TRUE;
}

boolean ring_selector(page *p, ring *r, page *up, int size, int cols)
{
    fputs("<table bgcolor=black cellpadding=1 cellspacing=0><tr><td>", p->out);
    fprintf(p->out, "<table bgcolor=%s cellpadding=%d><tr><td align=center>\n",
		p->bg, RINGSEL_PADDING);
    if (!ring_nextprev(p, r))
	return FALSE;
    fputs("<br>\n", p->out);
    if (!ring_navigation(p, r, up, STYLE_SELECT|STYLE_SHORT, size, cols, NULL))
	return FALSE;
    fputs("</td></tr></table>\n", p->out);
    fputs("</td></tr></table>\n", p->out);
    return TRUE;
}

boolean ring_nextprev(page *p, ring *r)
{
    list   *members = &r->members;
    member *mm, *mp, *mn;
    page   *pp, *pn;
    char   *lr, *lr0, *pp_title, *pn_title, *sep;
    char    imgbuf[MAX_FIELD], rp[MAX_FIELD], ip[MAX_FIELD], title[MAX_FIELD];
    int     ord, hs;

    if (debug > 2)
	fprintf(stderr, "ring_nextprev(p='%s', ring='%s')\n", p->h, r->path);

    if (p->ring == r)
    {
	if (debug > 3)
	    printf("  p and r match; p's ord = %d\n", p->ord);
	mm = p->member;
	ord = p->ord;
    }
    else
    {
	if (!(mm = find_member(members, p, &ord)))
	{
	    if (debug > 3)
		 puts("  p not in r's members");
	    ord = 0;
	}
    }

    if (!ord && !single_page)
    {
	printf("***** ERROR: page '%s' is not a member of ring '%s'\n",
		p->h, r->path);
	return FALSE;
    }

    /* figures relative paths to root directory and its images subdir */
    rel_path(p->h, IMAGES, ip);

    if (LIST_SIZE(members) > 1)
    {
	fprintf(p->out, "  <map name=nm%d>\n", ++p->nms);

	if (mm)
	{
	    mp = LIST_HEAD(members);
	    if (!(pp = mp->p))
		pp = ring_firstpage(mp->r);
	    sub_bigger("\"", "&quot;", pp->long_title, pp_title = title);
	    rel_path(p->h, pp->html, rp);
	}
	else
	{
	    pp_title = "Unknown";
	    strcpy(rp, "NO_LINK");
	}

	fprintf(p->out, "    <area href=\"%s\" shape=rect "
			"coords=\"11,22,27,34\" "
			"alt=\"First: %s\" "
			"title=\"First: %s\">\n",
		rp, pp_title, pp_title);

	if (mm)
	{
	    mp = LIST_TAIL(members);
	    if (!(pn = mp->p))
		pn = ring_lastpage(mp->r);
	    sub_bigger("\"", "&quot;", pn->long_title, pn_title = title);
	    rel_path(p->h, pn->html, rp);
	}
	else
	{
	    pn_title = "Unknown";
	    strcpy(rp, "NO_LINK");
	}

	fprintf(p->out, "    <area href=\"%s\" shape=rect "
			"coords=\"37,22,53,34\" "
			"alt=\"Last: %s\" "
			"title=\"Last: %s\">\n",
		rp, pn_title, pn_title);

	if (mm)
	{
	    if (!(mp = LINK_PREV(mm)))
		mp = LIST_TAIL(members);
	    if (!(pp = mp->p))
		pp = ring_firstpage(mp->r);
	    sub_bigger("\"", "&quot;", pp->long_title, pp_title = title);
	    rel_path(p->h, pp->html, rp);
	}
	else
	{
	    pp_title = "Unknown";
	    strcpy(rp, "NO_LINK");
	}

	fprintf(p->out, "    <area href=\"%s\" shape=rect "
			"coords=\"0,0,32,33\" "
			"alt=\"Prev: %s\" "
			"title=\"Prev: %s\">\n",
		rp, pp_title, pp_title);

	if (mm)
	{
	    if (!(mn = LINK_NEXT(mm)))
		mn = LIST_HEAD(members);
	    if (!(pn = mn->p))
		pn = ring_firstpage(mn->r);
	    sub_bigger("\"", "&quot;", pn->long_title, pn_title = title);
	    rel_path(p->h, pn->html, rp);
	}
	else
	{
	    pn_title = "Unknown";
	    strcpy(rp, "NO_LINK");
	}

	fprintf(p->out, "    <area href=\"%s\" shape=rect "
			"coords=\"33,0,63,33\" "
			"alt=\"Next: %s\" "
			"title=\"Next: %s\">\n",
		rp, pn_title, pn_title);

	fputs("  </map>\n", p->out);

	lr = ON(p, PAGE_INVERSE) ? LRINV_GIF : LR_GIF;
	fputs("  ", p->out);
	sprintf(imgbuf, "<img src=\"%s/%s\" usemap=\"#nm%d\""
			" width=%d height=%d border=0 alt=Navigation>",
			ip, lr, p->nms, NAVGIF_WIDTH, NAVGIF_HEIGHT);
	if (!write_link(rp, NULL, imgbuf, p))
	    return FALSE;
    }
    else /* nothing else in ring, just this one page, so grey out arrows */
    {
	lr0 = ON(p, PAGE_INVERSE) ? LR0INV_GIF : LR0_GIF;
	fprintf(p->out, "  <img src=\"%s/%s\" width=%d height=%d "
			"border=0 alt=Navigation>\n",
		ip, lr0, NAVGIF_WIDTH, NAVGIF_HEIGHT);
    }

    hs = NOTE_SIZE;
    sep = ": ";
    if (strlen(r->short_title) > 10)
    {
	hs = NOTE_SIZE - 1;
	sep = "<br>";
    }
    fprintf(p->out, "<br>\n  <font face=%s%s size=%d>%s%s",
		GENSITE_FONT, ON(p, PAGE_INVERSE) ? " color=white" : "",
		hs, r->short_title, sep);

    if (!ord)
    {
	printf("***** ERROR: page '%s' not in ring '%s'\n", p->h, r->path);
	return FALSE;
    }

    fprintf(p->out, "<font color=red><b>%d</b></font>"
		    "&nbsp;of&nbsp;%d</font>\n",
	    ord, LIST_SIZE(members));

    return TRUE;
}

member *find_member(list *members, page *p, int *ord)
{
    member *mm;
    page   *mp;
    int     o;

    for (o = 1, mm = LIST_HEAD(members); mm; mm = LINK_NEXT(mm), o++)
    {
	if (!(mp = mm->p))
	    mp = ring_firstpage(mm->r);
	if (mp == p)
	{
	    *ord = o;
	    return mm;
	}
    }
    *ord = 0;
    return NULL;
}

boolean ring_navigation(page *p, ring *r, page *up, unsigned style,
			int size, int cols, char *font)
{
    int     ci, rows, ri, i, mi, si;
    boolean tab;
    char    rp[MAX_FIELD], t[MAX_FIELD];
    member *m, *mc, *members[MAX_MEMBERS];
    char   *c, *d, *mark, *color, *title, *width;
    page   *pm;
    ring   *rm;

    if (debug > 2)
	fprintf(stderr, "-->   ring_navigation(p='%s', r='%s', style=%u)\n",
		p->h, r->path, style);

    if (!size)
    {
	size = SELECTOR_SIZE;
	if (LIST_SIZE(&r->members) >= 20)
	    size--;
    }

    if (!font)
	font = GENSITE_FIXED;

    /* scan members and form list of indexes (omit pages from long sects) */
    for (m = LIST_HEAD(&r->members), i = mi = 0, si = -1;
	 m; m = LINK_NEXT(m), i++)
    {
	if (!(pm = m->p))
	    pm = ring_firstpage(m->r);

	if (ON(pm, PAGE_SECTION))		/* reset for new section */
	    si = 0;
	if (si >= 0)				/* in section? */
	{
	    ++si;
	    if (si >= MAX_SECTION)
	    {
		if (si == MAX_SECTION)		/* mark omission */
		    members[mi++] = (member *) 0xDEADBEEF;
		continue;
	    }
	}
	members[mi++] = m;
    }

    if (!cols && !(cols = r->cols))		/* automatic columnation? */
	cols = ((mi - 1) / RINGNAV_ROWS) + 1;	/* # of columns needed */
    rows = mi / cols;				/* rows per column */
    if ((cols * rows) < mi)			/* make sure rounding */
	rows++;					/* didn't lose anyone. */

    if ((tab = !(style & STYLE_LI)))		/* if not <li>, as table */
    {
	if (style & STYLE_10OPCT)
	    width = "width=\"100%\" ";
	else
	    width="";

	fprintf(p->out, "<table %scellspacing=1 cellpadding=0 "
			"summary=\"%s\">\n", width, r->long_title);
    }

    /* for each row */
    for (m = members[0], i = ri = 0;
	 ri < rows; m = members[++i], ri++)
    {
	if (tab)
	    fputs("<tr>", p->out);

	/* for each column */
	for (mc = m, ci = 0, si = i; mc && (ci < cols); ci++)
	{
	    if (tab)
		fprintf(p->out, "<td><font face=%s size=%d>\n", font, size);

	    if (mc == (member *) 0xDEADBEEF)	/* omission */
		fputs("&nbsp;...</font>", p->out);
	    else
	    {
		if ((pm = mc->p))		/* member is a page */
		    title = (style & STYLE_SHORT) ?
			pm->short_title : pm->long_title;
		else				/* member is a ring */
		{
		    rm = mc->r;
		    title = (style & STYLE_SHORT) ?
			rm->short_title : rm->long_title;
		    pm = ring_firstpage(rm);
		}

		if (style & STYLE_SHORT)
		    sub_bigger(" ", "&nbsp;", title, t);
		else
		    strcpy(t, title);

		if (cols > 3)
		{
		    sub_smaller("Page&nbsp;", "", t);
		    sub_smaller("Inside&nbsp;", "I", t);
		    sub_smaller("Front&nbsp;Cover", "FC", t);
		    sub_smaller("Back&nbsp;Cover",  "BC", t);
		    sub_smaller("Front", "F", t);
		    sub_smaller("Back", "B", t);
		    sub_smaller("Gravure", "G", t);
		    sub_smaller("Dust&nbsp;Jacket", "DJ", t);
		}

		/* page wants special treatment? */
		if ((color = (pm == p) ? "red" : NULL))	/* current page? */
		    fprintf(p->out, "<font color=%s>", color);

		if (tab)
		{
		    mark = ON(pm, PAGE_SECTION) ? "&sect;" :
				(style & STYLE_BULLET) ? "<b>&middot;</b>" :
				    "&middot;";
		    fputs(mark, p->out);
		}
		else
		    fprintf(p->out, "<li><font face=%s size=%d>\n", font, size);

		if (pm == p)
		    fprintf(p->out, "<b>%s</b>", t);
		else
		{
		    rel_path(p->h, pm->html, rp);
		    if ((tab && ON(pm, PAGE_SECTION)) || (style & STYLE_BOLD))
			fputs("<b>", p->out);
		    if (!write_link(rp, NULL, t, p))
			return FALSE;
		    if ((tab && ON(pm, PAGE_SECTION)) || (style & STYLE_BOLD))
			fputs("</b>", p->out);
		}

		c = pm->cite; d = pm->date;
		if ((c && (style & STYLE_CITE)) ||
		    (d && (style & STYLE_DATE)))
		{
		    fputs(" <small>", p->out);
		    if (c && (style & STYLE_CITE))
			fprintf(p->out, " (%s)", c);
		    if (d && (style & STYLE_DATE))
			fprintf(p->out, " [%s]", d);
		    fputs("</small>", p->out);
		}

		if (color)
		    fputs("</font>", p->out);

		fputs("</font>\n", p->out);
	    }

	    fputs(tab ? "</td>\n" : "\n", p->out);

	    if (cols > 1)
	    {
		si += rows;
		mc = (si < mi) ? members[si] : NULL;
	    }
	}
	if (tab)
	    fputs("</tr>\n", p->out);
    }
    if (tab)
	fputs("</table>\n", p->out);

    if (debug > 2)
	fprintf(stderr, "<--   ring_navigation(p='%s', r='%s', style=%u)\n",
		p->h, r->path, style);

    return TRUE;
}

/* strip all <tags> */

void strip_tags(char *from, char *dest)
{
    int c;

    while ((c = *from++))
    {
	while (c == '<')			/* start tag? */
	{
	    while (((c = *from++)) && (c != '>'))
		;				/* skip to end */
	    c = *from++;			/* check next char */
	}
	*dest++ = c;				/* save non-tag chars */
	if (!c)
	    break;
    }
    *dest = 0;					/* tie off dest buf */
}

/* --------------------------------- image -------------------------------- */

/* return page pointer given its path.  pages are identified by the
   full path to their .h file.  if the named page already exists, it
   is just returned.  otherwise, adds a new page.  pages names are in
   the global hash table 'pages'.
 */

image *image_register(char *ref, char *image_path)
{
    char   pathbuf[MAX_PATH];
    image *i;

    rel_expand(image_path, ref, pathbuf);
    if ((i = hash_lookup(images, pathbuf)))	/* already exists? */
    {
	if (debug > 4)
	    fprintf(stderr, "image_register(ref='%s', image_path='%s') -> "
			    "W=%d H=%d\n", ref, image_path,
			    i->width, i->height);
	return i;
    }

    CALLOC(i, image);
    strcpy(i->path, pathbuf);		/* "foo.jpg" etc */

    /* ditto manifest */
    add_to_manifest(pathbuf);

    hash_add(images, i->path, (void *) i);
    n_images++;

    /* gonna needta sniff the image eventually */
    if (!(i->f = fopen(pathbuf, "rb")))
    {
	printf("***** ERROR: failed to open image \"%s\"\n", pathbuf);
	perror("fopen");
	return NULL;
    }

    if (has_extension(image_path, "gif"))
    {
	if (!gif_size(i))
	{
	    printf("***** ERROR: couldn't figure size of GIF \"%s\"\n",
			image_path);
	    return NULL;
	}
    }
    else if (has_extension(image_path, "jpg"))
    {
	if (!jpg_size(i))
	{
	    printf("***** ERROR: couldn't figure size of JPEG \"%s\"\n",
			image_path);
	    return NULL;
	}
    }
    else if (has_extension(image_path, "png"))
    {
	if (!png_size(i))
	{
	    printf("***** ERROR: couldn't figure size of PNG \"%s\"\n",
			image_path);
	    return NULL;
	}
    }
    else if (has_extension(image_path, "tif"))
    {
	if (!tif_size(i))
	{
	    printf("***** ERROR: couldn't figure size of TIF \"%s\"\n",
			image_path);
	    return NULL;
	}
    }
    else
    {
	printf("***** ERROR: unknown image type for \"%s\"\n", image_path);
	return NULL;
    }

    FCLOSE(i->f);

    return i;
}

boolean gif_size(image *i)
{
    char     gifbuf[6];
    unsigned w1, w2, h1, h2;

    if ((fread(gifbuf, sizeof(char), 6, i->f) < 6) ||
	(gifbuf[0] != 'G') || (gifbuf[1] != 'I') || (gifbuf[2] != 'F') ||
	!isalnum(gifbuf[3]) || !isalnum(gifbuf[4]) || !isalnum(gifbuf[5]))
    {
	printf("***** ERROR: file \"%s\" is not a GIF\n", i->path);
	return FALSE;
    }
    w1 = getc(i->f);
    w2 = getc(i->f);
    if (!(i->width = (w2 << 8) + w1))
	return FALSE;

    h1 = getc(i->f);
    h2 = getc(i->f);
    if (!(i->height = (h2 << 8) + h1))
	return FALSE;

    return TRUE;
}

boolean jpg_size(image *i)
{
    struct jpeg_decompress_struct jd;
    struct jpeg_error_mgr         jerr;

    /* set up jpeg decompression */
    jd.err = jpeg_std_error(&jerr);
    jpeg_create_decompress(&jd);
    jpeg_stdio_src(&jd, i->f);
    jpeg_read_header(&jd, TRUE);
    if (!(i->width = jd.image_width))
	return FALSE;
    if (!(i->height = jd.image_height))
	return FALSE;
    return TRUE;
}

boolean png_size(image *i)
{
    unsigned char sig[8];
    png_structp png_ptr;
    png_infop info_ptr;

    fread(sig, 1, 8, i->f);
    if (!png_check_sig(sig, 8))
        return FALSE;

    if (!(png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,
						NULL, NULL, NULL)))
	return FALSE;

    if (!(info_ptr = png_create_info_struct(png_ptr)))
    {
        png_destroy_read_struct(&png_ptr, NULL, NULL);
        return FALSE;
    }

    png_init_io(png_ptr, i->f);
    png_set_sig_bytes(png_ptr, 8);

    png_read_info(png_ptr, info_ptr);

    if (!(i->width = png_get_image_width(png_ptr, info_ptr)))
	return FALSE;
    if (!(i->height = png_get_image_height(png_ptr, info_ptr)))
	return FALSE;

    png_destroy_read_struct(&png_ptr, NULL, NULL);

    return TRUE;
}

boolean tif_size(image *i)
{
    TIFF* tif;
    uint32 imageWidth, imageLength;

    if (!(tif = TIFFFdOpen(fileno(i->f), i->path, "rb")))
	return FALSE;
    TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &imageWidth);
    i->width = imageWidth;
    TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &imageLength);
    i->height = imageLength;
    TIFFClose(tif);
    return TRUE;
}

/* image i [links -> to]
	<p>      page being created
	<image>  the image to be included
	<to>     if defined, image is a link to this
	<tag>    tag within <to> or NULL
	<args>   rest of <img> data: [img tags] <alt>
*/

boolean image_write(page *p, char *img, char *to, char *tag,
			char *args, int n_thumb)
{
    char  *rest, *colorname, alt[MAX_FIELD], params[MAX_FIELD], rp[MAX_PATH];
    image *id, *toi, *thumb;
    int    cmpnum;

    if (debug > 2)
	fprintf(stderr, "image_write(image=\"%s\", to=\"%s\", args=\"%s\")\n",
		img, STR(to), STR(args));

    if (!args)
    {
	puts("***** ERROR: NULL args to image_write");
	return FALSE;
    }

    /* if first arg is quoted string, it's a name for color comparison */
    if (!strcmp(p->h, "Gallery") && (*args == '"'))
    {
	for (colorname = ++args; *args != '"'; args++) ;
	*args++ = 0;
    }
    else
	colorname = NULL;

    /* skip over attr=value pairs until find beginning of alt text */
    if (!(rest = get_params(args, params)))
    {
	printf("***** ERROR: page \"%s\" image \"%s\" has no alt\n",
		p->h, img);
	return FALSE;
    }
    sub_bigger("\"", "&quot;", rest, alt);	/* replace " with &quot; */
    if (!(id = image_register(p->h, img)))
	return FALSE;

    if (colorname)
    {
	if (!n_thumb && !comparable(id, colorname))
	    return FALSE;
	strcat(alt, " in ");
	strcat(alt, colorname);
    }

    if (to)		/* hyperlink image to <to>? */
    {
	toi = NULL;	/* to image */
	if (memcmp(to, "http", 4))	/* not an xlink? */
	{
	    if (has_extension(to, "gif") || has_extension(to, "jpg") ||
		has_extension(to, "png"))
	    {
		if (!(toi = image_register(p->h, to)))
		    return FALSE;

		if (colorname && n_thumb)
		    if (!comparable(toi, colorname))
			return FALSE;

		id->thumb_of = toi;	/* id is a thumb of toi */
	    }
	    else if (!has_extension(to, "html") && !has_extension(to, "pdf"))
	    {
		printf("***** ERROR: bogus page \"%s\" link \"%s\"\n",
			p->h, to);
		return FALSE;
	    }
	    rel_path(p->h, toi ? toi->path : to, rp);
	    to = rp;
	}

	if (debug > 2)
	    fprintf(stderr, "  --> %s\n", to);

	if (tag && *tag)
	    fprintf(p->out, "<a href=\"%s#%s\">", to, tag);
	else
	    fprintf(p->out, "<a href=\"%s\">", to);
    }

    rel_path(p->h, id->path, rp);
    fprintf(p->out, "<img src=\"%s\" width=%d height=%d",
		rp, id->width, id->height);

    if (*params)
	fprintf(p->out, " %s", params);

    fprintf(p->out, " alt=\"%s\"", alt);

    if ((cmpnum = id->cmpnum) ||	/* there a comparator number? */
	((thumb = id->thumb_of) && (cmpnum = thumb->cmpnum)))
	fprintf(p->out, " title=\"%s {%d}\"", alt, cmpnum);
    else
	fprintf(p->out, " title=\"%s\"", alt);

    putc('>', p->out);

    if (to)
	fputs("</a>", p->out);

    return TRUE;
}

char *get_params(char *from, char *dest)
{
    char *rest, *arg, argbuf[MAX_FIELD];
    int n_tags;

    *dest = 0;
    for (n_tags = 0; ((arg = next_arg(from, argbuf, &rest))); from = rest)
    {
	if (strchr(arg, '='))		/* another "hspace=0" type tag? */
	{
	    if (n_tags++)		/* count them up */
		strcat(dest, " ");
	    strcat(dest, arg);
	}
	else
	    break;			/* should be 1st word of alt */
    }
    return from;
}

boolean comparable(image *i, char *colorname)
{
    if (i->colorname)	/* already have a color name? */
    {
	if (strcmp(i->colorname, colorname))	/* new one better match! */
	{
	    printf("***** ERROR: image '%s' has color name '%s' "
		    "*and* '%s'\n", i->path, i->colorname, colorname);
	    return FALSE;
	}
    }
    else /* this is first naming for color */
    {
	if (!single_page)
	    image_rgb(i);
	new_colors = TRUE;
	i->colorname = save_string(colorname);
	LINK_TAIL(&cmplist, i)
	i->cmpnum = LIST_SIZE(&cmplist);
    }
    return TRUE;
}

boolean image_rgb(image *i)
{
    char *error_string;

    if (!jpg_mean(i->path, NULL, &error_string,
		  -1, -1, -1, &i->red, &i->green, &i->blue,
		  NULL, NULL, (debug > 1)))
    {
	printf("Failed to calculate RGB average of '%s', error:", i->path);
	puts(error_string);
	return FALSE;
    }
    if (debug)
	fprintf(stderr, "RGB of '%s' is (%d,%d,%d)\n",
		i->path, i->red, i->green, i->blue);
    return TRUE;
}

/* --------------------------------- ring -------------------------------- */

/* register a new ring */

ring *ring_register(char *ref, char *ring_path, char *def_theme,
		    boolean primary, boolean solo)
{
    char ringbuf[MAX_PATH];
    ring *r;

    rel_expand(ring_path, ref, ringbuf);
    if ((r = hash_lookup(rings, ringbuf)))	/* already exists? */
    {
	if (debug > 3)
	    fprintf(stderr, "ring_register(ref='%s', ring_path='%s') -> '%s'\n",
			ref, ring_path, r->path);
	return r;
    }

    CALLOC(r, ring)
    if (debug > 1)
	fprintf(stderr, "new ring '%s' r='%p', def_theme=%s\n",
		ring_path, r, STR(def_theme));
    strcpy(r->path, ringbuf);			/* "foo.ring" */
    r->width = GENSITE_WIDTH;
    r->height = GENSITE_HEIGHT;
    r->theme = def_theme;

    hash_add(rings, r->path, (void *) r);
    n_rings++;

    /*if (!single_page || (level == 1))*/
	if (!ring_meta(r, primary, solo))
	    return NULL;

    if (debug > 2)
	fprintf(stderr, "new ring '%s' title='%s'\n", ring_path, r->long_title);

    return r;
}

/* return first page of ring, drilling down as needed */

page *ring_firstpage(ring *r)
{
    member *m1;

    while (r)
    {
	m1 = LIST_HEAD(&r->members);	/* first member */
	if (m1->p)			/* if it's a page, good */
	    return m1->p;		/* that's what we want */
	r = m1->r;			/* else must be a ring, try again */
    }
    return NULL;
}

/* return last page of ring, drilling down as needed */

page *ring_lastpage(ring *r)
{
    member *m1;

    while (r)
    {
	m1 = LIST_TAIL(&r->members);	/* first member */
	if (m1->p)			/* if it's a page, good */
	    return m1->p;		/* that's what we want */
	r = m1->r;			/* else must be a ring, try again */
    }
    return NULL;
}

/* note we pass in ring here with the full extension, a complete filename,
   so we can read from it directly.
 */
boolean ring_process(ring *r, page *up)
{
    char    titlebuf[MAX_FIELD];
    list   *members;
    member *m;
    page   *p;

    if (ON(r, RING_PROCESSED))
	return TRUE;

    if (debug)
	fprintf(stderr, "Processing ring (%d) '%s'\n", level, r->path);

    if (loading)
    {
	/* see sitemap formatting */
	strcpy(titlebuf, r->long_title);
	sub_smaller("<i>",  "", titlebuf);
	sub_smaller("</i>", "", titlebuf);
	sub_smaller("<b>",  "", titlebuf);
	sub_smaller("</b>", "", titlebuf);

	members = &r->members;

	switch (r->style)
	{
	    case RING_STYLE_DEFAULT:
		if (r != root_ring)
		{
		    fputs("<li>", sitemap->out);
		    if (level < 2)
			fputs("<big>", sitemap->out);
		    if (level < 3)
			fputs("<big>", sitemap->out);
		    fprintf(sitemap->out, "<i><b>%s</b>: %d</i>",
			titlebuf, LIST_SIZE(members));
		    if (level < 3)
			fputs("</big>", sitemap->out);
		    if (level < 2)
			fputs("</big>", sitemap->out);
		    putc('\n', sitemap->out);
		    fputs("<ul>\n", sitemap->out);
		}
		break;
	    case RING_STYLE_BOOK:
	    {
#if EVERY_PAGE
		char    shortname[MAX_FIELD];
		char   *title;

		fprintf(sitemap->out, "<li><b><i>%s</i></b><font size=%d>",
			titlebuf, NOTE_SIZE);
		for (m = LIST_HEAD(members); m; m = LINK_NEXT(m))
		{
		    if (!(p = m->p))
			p = ring_firstpage(m->r);
		    putc('\n', sitemap->out);
		    strip_tags(p->short_title, title = shortname);
		    sub_smaller("Page ",       "",   shortname);
		    sub_smaller("Pages ",      "",   shortname);
		    sub_smaller("Front Cover", "FC", shortname);
		    sub_smaller("I.F.Cover",   "IFC",shortname);
		    sub_smaller("Back Cover",  "BC", shortname);
		    sub_smaller("I.B.Cover",   "IBC",shortname);
		    sub_smaller("Inside",      "I",  shortname);
		    sub_smaller("Drawing",     "D",  shortname);
		    sub_smaller("Figures",     "Fig",  shortname);
		    if (strstr(shortname, "Gravure"))
		    {
			sub_smaller("Gravure",     "G",  shortname);
			sub_smaller("Front",       "F",  shortname);
			sub_smaller("Back",        "B",  shortname);
		    }
		    sub_smaller(" ",           "",   shortname);
		    if (ON(p, PAGE_SECTION))
			fputs("<b>", sitemap->out);
		    fprintf(sitemap->out, META_LINK_FORMAT, p->h, title);
		    if (ON(p, PAGE_SECTION))
			fputs("</b>", sitemap->out);
		}
		fputs("</font>\n", sitemap->out);
#else
		m = LIST_HEAD(members);
		if (!(p = m->p))
		    p = ring_firstpage(m->r);
		fputs("<li><i><b>", sitemap->out);
		fprintf(sitemap->out, META_LINK_FORMAT, p->h, titlebuf);
		fprintf(sitemap->out, "</b>: %d</i>\n", LIST_SIZE(members));
#endif
	    }
	}
    }

    if (!up)
    {
	if (r != root_ring)
	{
	    printf("***** ERROR: r='%s' has no up and not root.ring", r->path);
	    return FALSE;
	}
	up = root;
    }

    /* process ring members */
    if (!single_page)
    {
	for (m = (member *) LIST_HEAD(&r->members); m; m = LINK_NEXT(m))
	    if (!(m->p ? page_process(m->p, up, r, 0) :
			 ring_process(m->r, up)))
		return FALSE;
    }

    if (loading)
    {
	if (r->style == RING_STYLE_DEFAULT)
	{
	    if (r != root_ring)
		fputs("</ul>\n", sitemap->out);
	}
    }

    if (debug > 1)
	fprintf(stderr, "ring_process r='%s'\n", r->path);

    SET(r, RING_PROCESSED);

    return TRUE;
}

boolean ring_meta(ring *r, boolean primary, boolean solo)
{
    char    line[MAX_LINE], argbuf[MAX_FIELD];
    char   *rest, *key, *bar, *style, *e,
	   *color, *mempath, *width, *height;
    list   *members;
    member *m;
    page   *p0;
    FILE   *f;
    rstyle  s;
    int     len;

    if (!r)
	return FALSE;

    members = &r->members;
    if (LIST_SIZE(members))		/* already have ring metadata? */
	return TRUE;

    if (debug > 2)
	fprintf(stderr, "--> ring_meta r='%s', primary=%s, solo=%s\n",
		r->path, primary?"T":"F", solo?"T":"F");

    if (!r->theme)
	r->theme = THEME_COLOR;
    r->height = GENSITE_HEIGHT;
    r->style = RING_STYLE_DEFAULT;

    if (!(f = fopen(r->path, "r")))
    {
	fprintf(stderr, "failed to open ring file '%s'\n", r->path);
	return FALSE;
    }

    while (fgets(line, sizeof(line), f))
    {
	if (*line == '#')		/* comments start w/"#" */
	    continue;
	e = line + strlen(line) - 1;	/* last char */
	if (*e != '\n')
	{
	    line[MAX_LINE - 1] = 0;
	    printf("***** ERROR: input line too big: '%s'\n", line);
	    return FALSE;
	}
	*e = 0;

	if (debug > 3)
	    printf("    ring line: '%s'\n", line);

	key = next_arg(line, argbuf, &rest);
	if (rest)
	    while (isspace(*rest)) rest++;

	if (!strcmp(key, "@member"))		/* most common key first */
	{
	    mempath = next_arg(rest, argbuf, &rest);

	    if (debug > 3)
		printf("  @member %s\n", mempath);

	    CALLOC(m, member)

	    /* if member is a ring, it means first member in that ring */
	    if (has_extension(mempath, "ring"))
	    {
		/*if (!solo)*/
		    if (!(m->r = ring_register(r->path, mempath,
						r->theme, primary, solo)))
			return FALSE;
	    }
	    else if (has_extension(mempath, "h"))
	    {
		if (!(p0 = page_register(r->path, mempath, r->theme,
						FALSE, solo, NULL)))
		    return FALSE;

		if (debug > 3)
		    printf("  @member page is '%s'\n", p0->h);

		if (primary)		/* if primary linkage */
		{
		    /* sanity check: make sure page isn't already linked up */
		    if (p0->ring)
		    {
			printf("***** ERROR: page '%s' is in ring '%s'"
			       " *and* '%s'\n", p0->h, r->path, p0->ring->path);
			return FALSE;
		    }
		    p0->ring = r;
		    p0->member = m;
		    p0->ord = LIST_SIZE(members) + 1;
		    if (debug > 3)
			printf("    p0's ord is %d\n", p0->ord);
		}

		m->p = p0;
	    }
	    else
	    {
		printf("***** ERROR: ring member '%s' not .ring or .h\n",
			mempath);
		return FALSE;
	    }

	    LINK_TAIL(members, m)
	}
	else if (!strcmp(key, "@title"))
	{
	    if ((bar = strchr(rest, '|')))	/* "Full long title|Short" */
	    {
		len = bar - rest;
		strncpy(r->long_title, rest, len);
		r->long_title[len] = 0;
		strcpy(r->short_title, bar + 1);
	    }
	    else
	    {
		strcpy(r->long_title, rest);
		strncpy(r->short_title, rest, MAX_SHORT_FIELD);
		next_arg(r->short_title, argbuf, NULL);	/* chop first word */
	    }
	}
	else if (!strcmp(key, "@cite"))
	    r->cite = save_string(rest);
	else if (!strcmp(key, "@color"))
	    r->theme = save_string(rest);
	else if (!strcmp(key, "@width"))
	{
	    if (!atoi(rest))
	    {
		printf("***** ERROR: Invalid ring width '%s'.\n", rest);
		return FALSE;
	    }
	    r->width = save_string(rest);
	}
	else if (!strcmp(key, "@height"))
	{
	    if (!atoi(rest))
	    {
		printf("***** ERROR: Invalid ring height '%s'.\n", rest);
		return FALSE;
	    }
	    r->height = save_string(rest);
	}
	else if (!strcmp(key, "@cols"))
	    r->cols = atoi(rest);
	else if (!strcmp(key, "@style"))
	{
	    style = next_arg(rest, argbuf, &rest);
	    if (!strcmp(style, "book"))
		s = RING_STYLE_BOOK;
	    else
	    {
		printf("Unknown ring style '%s'\n", style);
		return FALSE;
	    }

	    switch (r->style = s)
	    {
		case RING_STYLE_BOOK:
		    r->bookbg = BODY_COLOR;
		    r->bookpage = THEME_COLOR;
		    if ((width = next_arg(rest, argbuf, &rest)))
		    {
			if (!atoi(width))
			{
			    printf("***** ERROR: Invalid ring width '%s'.\n",
					width);
			    return FALSE;
			}
			r->width = save_string(width);
			if ((height = next_arg(rest, argbuf, &rest)))
			{
			    if (!atoi(height))
			    {
				printf("***** ERROR: Invalid ring "
					"height '%s'.\n", height);
				return FALSE;
			    }
			    r->height = save_string(height);
			    if ((color = next_arg(rest, argbuf, &rest)))
			    {
				r->bookbg = save_string(color);
				if ((color = next_arg(rest, argbuf, &rest)))
				    r->bookpage = save_string(color);
			    }
			}
		    }
		    break;
		default:
		    /* no other arguments for rest */
		    break;
	    }
	}
	else
	{
	    printf("***** ERROR: unknown key '%s' in ring '%s'\n",
			key, r->path);
	    return FALSE;
	}
    }

    fclose(f);

    if (!LIST_SIZE(&r->members))
    {
	printf("***** ERROR: ring '%s' has no members\n", r->path);
	return FALSE;
    }

    return TRUE;
}

/* ---------------------------------- index --------------------------------- */

int generate_index()
{
    list  *l;
    entry *e, *last_e, *next_e;
    char  *see, *key;
    int    li, lj, subs, eord, letter, repeat, dup;
    char   dest[MAX_FIELD], seebuf[MAX_FIELD];
    char   letbuf[2], destbuf[3], repbuf[8];

    fprintf(index_page->out, "<center><table width=\"95%%\" "
	  "summary=\"%s\"><tr><td>\n", INDEX_TITLE);

    letbuf[1] = destbuf[2] = 0; destbuf[0] = '#';
    for (last_e = NULL, li = 0; li < INDEX_LETTERS; li++)
    {
	letter = 'A' + li;

	fprintf(index_page->out, "<a name=\"%c\"></a>", letter);
	fprintf(index_page->out, "<center><table width=\"100%%\" "
		"cellpadding=%d cellspacing=%d "
		"summary=\"A-Z Navigation Bar\">\n",
		INDEXBAR_PADDING, INDEXBAR_SPACING);
	fputs("<tr align=center style=\"font-family:sans\">\n", index_page->out);
	for (lj = 0; lj < INDEX_LETTERS; lj++)
	{
	    letbuf[0] = destbuf[1] = 'A' + lj;

	    if (li == lj)
	    {
		fprintf(index_page->out, "<td bgcolor=black width=\"%d%%\" style=\"color:%s\">",
		    strchr("FIJ", *letbuf) ? 3 : 4, INDEX_COLOR);
		fprintf(index_page->out, "<b>%s</b></td>\n", letbuf);
	    }
	    else
	    {
		fprintf(index_page->out, "<td bgcolor=\"%s\" width=\"%d%%\"><b>",
			INDEX_COLOR, strchr("FIJ", *letbuf) ? 3 : 4);
		fprintf(index_page->out, META_LINK_FORMAT, destbuf, letbuf);
		fputs("</b></td>\n", index_page->out);
	    }
	}
	fputs("</tr></table></center>\n\n", index_page->out);

	l = &entries[li];
	if (!LIST_SIZE(l))
	{
	    fputs("<dl><dd style=\"font-size:200%\">&empty;</dd></dl>\n\n", index_page->out);
	    continue;
	}

	fputs("<ul>\n", index_page->out);
	subs = eord = dup = 0;
	repeat = 1;
	for (e = (entry *) LIST_HEAD(l); e; last_e = e, e = next_e)
	{
	    next_e = (entry *) LINK_NEXT(e);

	    if (last_e && subs && strcmp(last_e->primary, e->primary))
	    {
		fputs("</ul>", index_page->out);
		subs = 0;			/* back to top level */
	    }

	    if ((key = e->secondary))		/* key|subkey? */
	    {
		if (!subs++)			/* first sub? */
		{
		    if (last_e && strcmp(last_e->primary, e->primary))
		    {
			fputs("<li>", index_page->out);
			fputs(e->primary, index_page->out);
		    }
		    fputs("\n<ul>", index_page->out);
		}
	    }
	    else
		key = e->primary;

	    if (e->see)				/* see also? */
	    {
		fputs("<li>", index_page->out);
		fprintf(index_page->out, "%s, see ", key);
		if (!(see = find_index(e->see, seebuf)))
		{
		    printf("***** ERROR: find_index \"%s\" failed\n", e->see);
		    return FALSE;
		}
		fprintf(index_page->out, META_LINK_FORMAT, see, e->see);
	    }
	    else
	    {
		if (e->tag)
		    sprintf(dest, "%s#%s", e->page->html, e->tag);
		else
		    sprintf(dest, "%s", e->page->html);

		if (!dup)
		    fputs("<li>", index_page->out);
		if (e->main)
		    fputs("<b>", index_page->out);
		if (e->image)
		    fputs("<i>", index_page->out);
		if (dup)
		{
		    fputs(" [", index_page->out);
		    sprintf(repbuf, "%d", ++repeat);
		    fprintf(index_page->out, META_LINK_FORMAT, dest, repbuf);
		    fputc(']', index_page->out);
		}
		else
		{
		    repeat = 1;
		    if (e->comment)		/* () part? */
		    {
			fprintf(index_page->out, META_LINK_FORMAT, dest, key);
			fputs(" (", index_page->out);
			fputs(e->comment, index_page->out);
			fputc(')', index_page->out);
		    }
		    else
			fprintf(index_page->out, META_LINK_FORMAT, dest, key);
		}
		if (e->image)
		    fputs("</i>", index_page->out);
		if (e->main)
		    fputs("</b>", index_page->out);
	    }
	    if (!(dup = next_e && !strcmp(e->text, next_e->text)))
		fputc('\n', index_page->out);
	}
	if (subs)
	    fputs("</ul>", index_page->out);
	fputs("</ul>\n", index_page->out);
    }
    fputs("</td></tr></table></center>\n", index_page->out);
    return TRUE;
}

char *find_index(char *text, char *dest)
{
    list  *l;
    entry *e;
    int    li, eord;

    for (li = 0; li < INDEX_LETTERS; li++)
    {
	l = &entries[li];
	for (e = (entry *) LIST_HEAD(l), eord = 0; e;
	     e = (entry *) LINK_NEXT(e), eord++)
	{
	    if (!strcmp(e->text, text))
	    {
		sprintf(dest, "%s#%s", e->page->html, e->tag);
		return dest;
	    }
	}
    }
    return NULL;
}

/* ----------------------------- style mask ---------------------------- */

/* turn string "foo|bar|baz" into mask STYLE_FOO|STYLE_BAR|STYLE_BAZ */

unsigned style_mask(char *s)
{
    char    *next;
    unsigned m;

    for (m = 0; s; s = next)
    {
	/* if there's a |, split there and note remainder for next time */
	if ((next = strchr(s, '|')))
	    *next++ = 0;

	if (!strcmp(s, "bullet"))	 m |= STYLE_BULLET;
	else if (!strcmp(s, "cite"))	 m |= STYLE_CITE;
	else if (!strcmp(s, "date"))	 m |= STYLE_DATE;
	else if (!strcmp(s, "li"))	 m |= STYLE_LI;
	else if (!strcmp(s, "nextprev")) m |= STYLE_NEXTPREV;
	else if (!strcmp(s, "sans"))     m |= STYLE_SANS;
	else if (!strcmp(s, "select"))	 m |= STYLE_SELECT;
	else if (!strcmp(s, "short"))	 m |= STYLE_SHORT;
	else if (!strcmp(s, "100"))	 m |= STYLE_10OPCT;
	else if (!strcmp(s, "bold"))	 m |= STYLE_BOLD;
	else
	{
	    printf("***** ERROR: unknown style flag '%s'\n", s);
	    return 0;
	}
    }
    return m;
}

/* ----------------------------- comparison list --------------------------- */

boolean load_cmplist(void)
{
    FILE    *f;
    image   *i;
    char     line[MAX_LINE], path[MAX_PATH], colorname[MAX_FIELD];
    unsigned r, g, b;
    int      n;

    if (!(f = fopen(CMP_LIST, "r")))
    {
	printf("No cmplist '%s', creating...\n", CMP_LIST);
	return TRUE;
    }

    if (debug)
	printf("Reading image list '%s'...\n", CMP_LIST);
    while (fgets(line, sizeof(line), f))
    {
	n = sscanf(line, "%s %2X%2X%2X %[^\n]\n", path, &r, &g, &b, colorname);
	if (n != 5)
	{
	    printf("Bad cmplist line: %s\n", line);
	    fclose(f);
	    return FALSE;
	}

	if (!(i = image_register(NULL, path)))
	{
	    fclose(f);
	    return FALSE;
	}

	i->red = r;
	i->green = g;
	i->blue = b;
	i->colorname = save_string(colorname);
	LINK_TAIL(&cmplist, i)
	i->cmpnum = LIST_SIZE(&cmplist);
    }
    fclose(f);

    if (debug)
	printf("Loaded %d color-named images\n", (int) LIST_SIZE(&cmplist));

    return TRUE;
}

boolean write_cmplist(void)
{
    FILE  *f;
    image *i;
    int    n;

    if (!(f = fopen(CMP_LIST, "w")))
    {
	printf("Failed to open images '%s'\n", CMP_LIST);
	return FALSE;
    }

    for (n = 0, i = LIST_HEAD(&cmplist); i; i = LINK_NEXT(i), n++)
	fprintf(f, "%s %02X%02X%02X %s\n", i->path, (unsigned) i->red,
		(unsigned) i->green, (unsigned) i->blue, i->colorname);
    fclose(f);

    if (debug)
	printf("Wrote %d color-named images\n", n);

    return TRUE;
}

/* ---------------------------------- path --------------------------------- */

/* case (1)

   given relative path	"./arf.h" and
   reference path	"foo/bar/baz.ring"...

   the '.' refers to ref's *directory*, so puts into buf:

			"foo/bar/arf.h"

   case (2)

   given relative path	"../arf.h" and
   reference path	"foo/bar/baz.ring"...

    the '..' refers to ref's *parent directory*, so puts into buf:

			"foo/arf.h"
 */

char *rel_expand(char *rel, char *ref, char *buf)
{
    char *to, *slash, *last_slash;
    int   c;

    if (debug > 3)
	fprintf(stderr, "rel_expand(rel='%s' ref='%s')\n", STR(rel), STR(ref));

    if ((*rel == '.') && ref)	/* relative to ref */
    {
	if (rel[1] == '.')	/* ../foo? */
	{
	    for (last_slash = slash = NULL, to = buf; ((c = *ref++)); )
	    {
		if ((*to++ = c) == '/')
		{
		    last_slash = slash;
		    slash = to;
		}
	    }
	    if (!last_slash)
		last_slash = buf;
	    strcpy(last_slash, rel + 3);
	}
	else			/* ref is "./foo" */
	{
	    for (slash = to = buf; ((c = *ref++)); )
		if ((*to++ = c) == '/')
		    slash = to;		/* copy, noting last slash */
	    strcpy(slash, rel + 2);	/* replace file part of ref */
	}
    }
    else
	strcpy(buf, rel);	/* not relative */

    if (debug > 3)
	fprintf(stderr, "    -> buf='%s'\n", buf);

    return buf;
}

/* split "a/b/foo.html" into "a", "b", "foo.html" and return 3 (#parts) */

int path_split(char *path, char *pathbuf, char *parts[])
{
    char *slash, *from;
    int   n;

    strcpy(pathbuf, path);		/* copy so can chop it up in place */
    for (n = 0, from = pathbuf; ((slash = strchr(from, '/')));
	 n++, from = ++slash)
    {					/* there's another slash & subpart */
	*slash = 0;			/* tie off part */
	parts[n] = from;		/* parts[] points to each piece */
    }
    parts[n++] = from;			/* final file is last part */
    return n;				/* return total # parts */
}

/* given from="a/b/foo.gif" and to="a/c/page.html" yields "../c/page.html" */

char *rel_path(char *from, char *to, char *buf)
{
    char  frombuf[MAX_PATH], tobuf[MAX_PATH];
    char *frompath[MAX_REL_DEPTH], *topath[MAX_REL_DEPTH];
    int   fl;			/* 'from' levels */
    int   tl;			/* 'to' levels */
    int   divl;			/* divergence level */
    int   ul;			/* up levels */
    int   dl;			/* down levels */
    int   min_level;		/* shortest depth */
    int   i;			/* generic loop index */

    fl = path_split(from, frombuf, frompath);	/* split paths on slashes */
    tl = path_split(to, tobuf, topath);

    *buf = 0;			/* start off destination buffer */

    min_level = (fl < tl) ? fl : tl;
    for (divl = 0, i = 0; i < min_level; i++)
    {
	if (strcmp(frompath[i], topath[i]))
	    break;		/* paths diverge here, else... */
	divl++;			/* still the same; divergence is higher */
    }

    if ((ul = fl - divl - 1))		/* number of up levels */
	for (i = 1; i <= ul; i++)
	    strcat(buf, "../");

    if ((dl = tl - divl - 1))		/* number of down levels */
    {
	for (i = divl; dl-- > 0; i++)
	{
	    strcat(buf, topath[i]);
	    strcat(buf, "/");
	}
    }

    strcat(buf, topath[tl - 1]);	/* add final file component */

    if (debug > 3)
	fprintf(stderr, "    rel_path(from='%s', to='%s') = '%s'\n",
		from, to, buf);

    return buf;
}

void page_path(page *p, page *cur, char *pathbuf, int depth)
{
    page *up;
    char  rp[MAX_PATH];

    if ((up = cur->up))
    {
	if (depth >= MAX_PAGE_PATH)
	    strcat(pathbuf, "... ");
	else
	{
	    page_path(p, up, pathbuf, depth + 1);
	    strcat(pathbuf, " &nbsp;<b>&gt;</b> ");
	}
    }
    if (depth)
    {
	strcat(pathbuf, "<a href=\"");
	rel_path(p->h, cur->html, rp);
	strcat(pathbuf, rp);
	strcat(pathbuf, "\">");
	strcat(pathbuf, cur->short_title);
	strcat(pathbuf, "</a>");
    }
    else
	strcat(pathbuf, cur->short_title);
}

/* --------------------------------- syntax -------------------------------- */

char *next_arg(char *from, char *dest, char **rest)
{
    char *to;
    int   c;

    if (!from || !dest)
	return NULL;
    while (isspace(*from))
	from++;					/* skip leading white */
    for (to = dest; (c = *from) && !isspace(c); )
    {
	*to++ = c;
	from++;
	if (c == '"')
	{
	    while ((c = *from))
	    {
		from++;
		if ((*to++ = c) == '"')
		    break;
	    }
	}
    }
    *to = 0;					/* tie off */
    if (rest)					/* if want rest of line */
    {
	if (c)
	{
	    while (isspace(*from))		/* skip trailing white */
		from++;
	    *rest = from;
	}
	else
	    *rest = NULL;
    }
    return dest;
}

char *lowercaseify(char *buf)
{
    char *p;
    int   c;

    for (p = buf; ((c = *p)); p++)		
	if (isupper(c))
	    *p = tolower(c);
    return buf;
}

boolean has_extension(char *s, char *ext)
{
    char *e = s + strlen(s) - 1;	/* point to end of s */
    int   extlen = strlen(ext);

    for (e = s + strlen(s) - 1; (e >= s) && (*e != '.'); e--)
	;				/* back up to '.' */

    return (*e == '.') && !memcmp(e + 1, ext, extlen) &&
	(!e[extlen + 1] || (e[extlen + 1] == '#'));
}

char *save_string(char *s)
{
    char *p;

    if ((p = malloc(strlen(s) + 1)))
	strcpy(p, s);
    return p;
}

/* sub "old" with "new" into newbuf since new is bigger than old */

char *sub_bigger(char *oldsub, char *newsub, char *buf, char *newbuf)
{
    int   newlen = strlen(newsub), oldlen = strlen(oldsub);
    int   skip_len;
    char *to, *from, *p;

    from = buf;
    to = newbuf;
    while ((p = strstr(from, oldsub)))
    {
	if ((skip_len = p - from))
	{
	    memcpy(to, from, skip_len);
	    to += skip_len;
	}
	memcpy(to, newsub, newlen);
	to += newlen;
	from = p + oldlen;
    }
    if (*from)	/* leftover? */
	strcpy(to, from);
    else
	*to = 0; /* tie off */
    return newbuf;
}

/* sub "old" with "new" in place, since new is smaller (or same size) */

void sub_smaller(char *oldsub, char *newsub, char *buf)
{
    int   newlen = strlen(newsub), oldlen = strlen(oldsub);
    char *to, *from, *p, *old;

    to = from = buf;
    while ((p = strstr(from, oldsub)))
    {
	if (newlen)
	    memcpy(p, newsub, newlen);
	from = to = p + newlen;
	old = p + oldlen;
	while ((*to++ = *old++)) ;
    }
}

/* ------------------------------- hash table ------------------------------ */

void *hash_lookup(hash h, char *key)
{
    unsigned v;
    bucket  *b;

    for (v = hash_of(key), b = h[v]; b; b = b->next)
	if (!strcmp(b->key, key))
	    return b->data;
    return NULL;
}

void hash_add(hash h, char *key, void *data)
{
    unsigned v;
    bucket  *b;

    v = hash_of(key);
    MALLOC(b, bucket);
    b->key = key;
    b->data = data;
    b->next = h[v];
    h[v] = b;
}

unsigned hash_of(char *s)
{
    unsigned v = 0;
    unsigned c;
    char    *from;

    if ((from = s))
	while ((c = (unsigned) *from++))
	    v = ((v << 4) ^ c) % MAX_HASH;
    if (debug > 4)
	fprintf(stderr, "hash_of('%s')=%u:%d\n", s, v, MAX_HASH);
    return v;
}

void hash_stats(hash h)
{
    int     i, len, len_total, n_chain;
    bucket *b;

    for (n_chain = len_total = i = 0; i < MAX_HASH; i++)
    {
	if ((b = (bucket *) h[i]))
	{
	    n_chain++;			/* something in this chain */
	    for (len = 0; b; b = (bucket *) b->next)
		len++;
	    len_total += len;
	}
    }

    printf("%4d chains of %d = %2d%% full, ",
	n_chain, MAX_HASH, (n_chain * 100) / MAX_HASH);

    printf("avg = %g\n", ((float) len_total) / n_chain);
}

void hash_dump(hash h)
{
    int     i;
    bucket *b;

    for (i = 0; i < MAX_HASH; i++)
    {
	if ((b = (bucket *) h[i]))
	{
	    printf("%04d: ", i);
	    for (; b; b = (bucket *) b->next)
		printf("\"%s\" ", b->key);
	    putchar('\n');
	}
    }
}

/* $Id: gensite.c,v 1.157 2022/04/18 13:53:52 ian Exp $ */
