/*
 *   Copyright (c) 1996 David Parsons. All rights reserved.
 *   
 *   Redistribution and use in source and binary forms, with or without
 *   modification, are permitted provided that the following conditions
 *   are met:
 *  1. Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in
 *     the documentation and/or other materials provided with the
 *     distribution.
 *  3. All advertising materials mentioning features or use of this
 *     software must display the following acknowledgement:
 *     
 *   This product includes software developed by David Parsons
 *   (orc@pell.chi.il.us)
 *
 *  4. My name may not be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *     
 *  THIS SOFTWARE IS PROVIDED BY DAVID PARSONS ``AS IS'' AND ANY
 *  EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 *  THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 *  PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DAVID
 *  PARSONS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 *  TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 *  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 *  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
 *  IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 *  THE POSSIBILITY OF SUCH DAMAGE.
 */

static const char rcsid[] = "$Id: s..del-html.c~6e8bad4594544bc4 1.6 99/11/24 01:00:01-00:00 orc $";

/*
 * html: html help file scanner/parser.
 *
 * Bugs: This is a recursive descent parser, so we can't safely breakpoint
 * it and display only part of a file.
 */
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>

#include "html.h"
#include "bytecodes.h"

static char text[MAXLEN];


/*
 * lookup a word to see if it's something special
 */
int
lookup(char *text)
{
    int negate = (*text == '/') ? 1 : 0;

    if (negate) ++text;

    switch (*text) {
    case '!':
	    if (text[1] == '-' && text[2] == '-' && text[3] == 0)
		if (!negate)
		    return BANGDASHDASH;
	    break;
    case '-':
	    if (text[1] == '-' && text[2] == 0)
		if (!negate)
		    return DASHDASH;
	    break;
    case 'A': case 'a':
	    if (text[1] == 0)
		return negate ? -A : A;
	    else if (!negate && strcasecmp(1+text, "lign") == 0)
		return ALIGN;
	    break;
    case 'B': case 'b':
	    if (text[1] == 0)
		return negate ? -BOLD : BOLD;
	    else if ((text[1] == 'R' || text[1] == 'r') && text[2] == 0) {
		if (!negate)
		    return BREAK;
	    }
	    else if (strcasecmp(1+text, "ody") == 0)
		return negate ? -BODY : BODY;
	    else if (strcasecmp(1+text, "lockquote") == 0)
		return negate ? -BQ : BQ;
	    break;
    case 'C': case 'c':
	    if (strcasecmp(1+text, "enter") == 0)
		return negate ? -CENTER : CENTER;
	    break;
    case 'D': case 'd':
	    if (text[2] != 0)
		break;
	    if (text[1] == 'l' || text[1] == 'L')
		return negate ? -DL : DL;
	    else if (text[1] == 't' || text[1] == 'T')
		return negate ? -DT : DT;
	    else if (text[1] == 'd' || text[1] == 'D')
		return negate ? -DD : DD;
	    break;
    case 'H': case 'h':
	    if (isdigit(text[1]) && text[2] == 0) {
		state.header_type = text[1]-'0';
		return negate ? -HEADER : HEADER;
	    }
	    else if ((text[1] == 'r' || text[1] == 'R') && text[2] == 0)
		return negate ? -HR : HR;
	    else if (strcasecmp(1+text, "ref") == 0) {
		if (!negate)
		    return HREF;
	    }
	    else if (strcasecmp(1+text, "tml") == 0)
		return negate ? -HTML : HTML;
	    else if (strcasecmp(1+text, "ead") == 0)
		return negate ? -HEAD : HEAD;
	    break;
    case 'I': case 'i':
	    if (text[1] == 0)
		return negate ? -ITAL : ITAL;
	    else if ((text[1] == 'd' || text[1] == 'D') && text[2] == 0) {
		if (!negate)
		    return ID;
	    }
	    break;
    case 'L':
	    if (!negate && strcasecmp(text+1, "elt") == 0)
		return LEFT;
	    break;
    case 'N': case 'n':
	    if (!negate && strcasecmp(1+text, "ame") == 0)
		return NAME;
	    break;
    case 'P': case 'p':
	    if (text[1] == 0)
		return negate ? -PARA : PARA;
	    else if (strcasecmp(1+text, "re") == 0)
		return negate ? -PRE : PRE;
	    break;
    case 'R':
	    if (!negate && strcasecmp(text+1, "ight") == 0)
		return RIGHT;
	    break;
    case 'T': case 't':
	    if (text[2] == 0 && (text[1] == 'T' || text[1] == 't'))
		return negate ? -TT : TT;
	    if (strcasecmp(1+text, "itle") == 0)
		return negate ? -TITLE : TITLE;
	    break;
    case 'W': case 'w':
	    if (!negate && strcasecmp(1+text, "idth") == 0)
		return WIDTH;
	    break;
    }
    return WORD;
} /* lookup */



static int pushback = 0;

/*
 * unscan() pushes a token back onto the input stream.  You can push
 * back as many as you want, but only the last one will be saved.
 */
void
unscan(int text, FILE *f)
{
    pushback = text;
} /* unscan */


/*
 * grab a token off our input stream
 */
int
scan(FILE *f)
{
    register c;
    register st = WORD;
    int x = 0;
    int did_escape = 0;
    static int brace_level=0;

    if (pushback) {
	st = pushback;
	pushback = 0;
	goto endofscan;
    }
    memset(text, 0, sizeof text);

    while ((c = getc(f)) != EOF) {
	if (isspace(c)) {
	    if (x == 0) {
		do {
		    text[x++] = c;
		} while ((c=getc(f)) != EOF && isspace(c));
		text[x] = 0;
		ungetc(c,f);
		st = SPACE; goto endofscan;
	    }
	    else {
		ungetc(c, f);
		break;
	    }
	}
	if (x < sizeof text)
	    text[x++] = c;
	if (c == '<' || c == '>' || c == '=') {
	    /* handle < & > */
	    if (x == 1) {
		switch (c) {
		case '<':	st = LT; brace_level++;	break;
		case '>':	st = GT;
				if (brace_level > 0)
				    brace_level--;
				break;
		case '=':	st = EQ;	break;
		}
		break;
	    }
	    else {
		--x;
		ungetc(c, f);
		break;
	    }
	}
	else if (c == '&') {
	    char little[20];
	    int lx = 0;

	    if (x != 1) {
		--x;
		ungetc(c, f);
		break;
	    }
	    did_escape = 1;
	    while ((c=getc(f)) != EOF && c != ';' && !isspace(c)) {
		if (lx < (sizeof little) - 1)
		    little[lx++] = c;
	    }
	    if (c != ';')
		ungetc(c, f);
	    little[lx] = 0;

	    if (x < sizeof text) {
		if (strcasecmp(little, "lt") == 0)
		    text[x-1] = '<';
		else if (strcasecmp(little, "gt") == 0)
		    text[x-1] = '>';
		else if (strcasecmp(little, "amp") == 0)
		    text[x-1] = '&';
		else if (strcasecmp(little, "emdash") == 0) {
		    text[x-1] = '-';
		    text[x++] = '-';
		}
		else if (little[0] == '#') {
		    for (c = 0, lx = 1; little[lx]; lx++)
			c = (c*10) + (little[lx] - '0');
		    text[x-1] = c;
		}
	    }
	}
	else if (c == '"' && brace_level > 0) {
	    /* snarf up strings */
	    --x;
	    if (x > 0) {
		ungetc(c,f);
		break;
	    }
	    while ((c = getc(f)) != EOF && c != '"')
		if (x < sizeof text)
		    text[x++] = c;
	}
    }
    if (c == EOF && x == 0)
	return YYEOF;
endofscan:
    if (x < sizeof text)
	text[x] = 0;
    if (st == WORD && !did_escape)
	st = lookup(text);

    return st;
} /* scan */


/*
 * scannw() grabs a token off our input stream, ignoring whitespace
 */
int
scannw(FILE *input)
{
    int tok;

    while ((tok = scan(input)) == SPACE)
	;
    return tok;
} /* scannw */


/*
 * block() changes indent and width for a BLOCKQUOTE section
 */
void
block()
{
    if (state.width > 8 ) {
	state.indent += 4;
	state.width -= 8;
    }
} /* block */


void eattag(FILE *);

/*
 * a_header() processes an A tag, returning a reference to any HREF=
 * found inside it.
 */
int
a_header(FILE *input)
{
    int tok;
    int tagid = EOF;

    while ((tok = scannw(input)) != GT && tok != YYEOF) {
	if (tok == NAME) {
	    if ((tok = scannw(input)) == EQ) {
		/* save name for later access */
		if ((tok = scannw(input)) == GT)
		    unscan(tok, input);
		else
		    addlabel(text);
	    }
	    else unscan(tok, input);
	}
	else if (tok == HREF) {
	    if ((tok = scannw(input)) == EQ) {
		if ((tok = scannw(input)) == GT)
		    unscan(tok, input);
		else if (tagid == EOF)
		    state.href = tagid = addhref(text);
	    }
	    else unscan(tok, input);
	}
    }
    return tagid;
} /* a_header */


/*
 * block_header() processes any blocking tag (P, BLOCKQUOTE, Hx, DL) and
 * deals with any ID or ALIGN's found inside it.
 */
void
block_header(FILE *input, int allow_align)
{
    int tok;

    while ((tok=scannw(input)) != GT && tok != YYEOF) {
	if (tok == ID) {
	    if ((tok = scannw(input)) == EQ) {
		if ((tok = scannw(input)) != GT)
		    addlabel(text);
		else
		    unscan(tok, input);
	    }
	    else
		unscan(tok, input);
	}
	else if ((tok == ALIGN) && allow_align) {
	    tok = scannw(input);
	    if (tok == EQ) {
		switch (tok = scannw(input)) {
		case LEFT:	state.align = LEFT;	break;
		case RIGHT:	state.align = RIGHT;	break;
		case CENTER:	state.align = CENTER;	break;
		default:	unscan(tok, input);
		}
	    }
	    else
		unscan(tok, input);
	}
    }
} /* block_header */


/* process a <HTML> .. </HTML> block */
void
do_html(FILE *input)
{
    block_header(input, 0);
    parse_it(input, -HTML, 0, BIT(HEAD)|BIT(BODY));
} /* do_html */


/* process a <HEAD> .. </HEAD> block */
void
do_head(FILE *input)
{
    block_header(input, 0);
    parse_it(input, -HEAD, 0, BIT(TITLE));
} /* do_head */


/* process a <BODY> .. </BODY> block */
void
do_body(FILE *input)
{
    block_header(input, 0);
    parse_it(input, -BODY, 0, ALL_BODY_TAGS);
} /* do_body */


/* process a <Hx> .. </Hx> block */
void
do_header(FILE *input, int header_type)
{
    struct Format sv;

    Save(sv);

    state.align = CENTER;

    if (header_type < 2)
	state.flags |= St_CAPS;

    block_header(input, 1);
    breakline();
    if (header_type < 4)
	addbcf(bcfSET_B);
    parse_it(input, -HEADER, header_type, ALL_BODY_TAGS);
    if (header_type < 4)
	addbcf(bcfCLEAR_B);
    breakline();

    Restore(sv);
} /* do_header */


/* process a <TITLE> .. </TITLE> block */
void
do_title(FILE *input)
{
    struct Format sv;
    Save(sv);

    state.doing = D_TITLE;

    start_title();
    block_header(input, 0);
    parse_it(input, -TITLE, 0, BIT(TITLE));
    Restore(sv);
} /* do_title */


/* process a <A...> .. </A> block */
void
do_a(FILE *input)
{
    struct Format sv;
    int tagid;

    Save(sv);

    state.flags = DF_A;

    tagid = a_header(input);
    parse_it(input, -A, 0, ALL_BODY_TAGS);

    if (tagid != EOF)
	endhref(tagid);

    Restore(sv);
} /* do_a */


/* process a <PRE> .. </PRE> block */
void
do_pre(FILE *input)
{
    struct Format sv;
    Save(sv);

    breakline();
    state.doing = D_PRE;
    set_tt();
    block_header(input, 0);
    parse_it(input, -PRE, 0, BIT(ITAL)|BIT(TT)|BIT(BOLD)|BIT(HR));
    clear_tt();
    Restore(sv);
} /* do_pre */


/* process a <BLOCKQUOTE> ... </BLOCKQUOTE> block */
void
do_bq(FILE *input)
{
    struct Format sv;

    Save(sv);
    breakline();
    block();
    block_header(input, 1);
    parse_it(input, -BQ, 0, ALL_BODY_TAGS);
    breakline();
    newline();
    Restore(sv);
} /* do_bq */


/* process a <P> ... </P> block */
void
do_paragraph(FILE *input, int tok)
{
    struct Format sv;

    breakline();
    Save(sv);
    if (tok == CENTER)
	state.align = CENTER;
    block_header(input, 1);
    parse_it(input, -tok, 0, ALL_BODY_TAGS);
    breakline();
    newline();
    Restore(sv);
} /* do_paragraph */


/* do_list handles definition lists, in a terrifyingly ugly fashion */
void
do_list(FILE *input)
{
    struct Format sv;

    breakline();
    Save(sv);
    state.flags = 0;
    eattag(input);
    parse_it(input, -DL, 0, ALL_BODY_TAGS);
    breakline();
    newline();
    Restore(sv);
} /* do_list */


/* do_bullet handles a <DT> tag and the following text */
void
do_bullet(FILE *input)
{
    breakline();	/* push out any cached text */
				/* then set the indentation */
    state.indent = (state.parent)->indent;
    state.width = (state.parent)->width;
    state.flags = DF_DT;	/* and flag ourself */
} /* do_bullet */


/* do_text handles a <DD> tag and the following text */
void
do_text(FILE *input)
{
    breakline();
    state.indent = (state.parent)->indent + 10;
    state.width = (state.parent)->width - 10;
} /* do_text */


/* do_hr handles a <HT> tag: this is renderer-specific */
void
do_hr(FILE *input)
{
    long width=100;
    char hr[201];
    int tok;
    struct Format sv;

    while ((tok = scannw(input)) != YYEOF && tok != GT) {
	if (tok == WIDTH) {
	    tok = scannw(input);
	    if (tok == EQ) {
		tok = scannw(input);
		if (tok == WORD && strchr(text, '%') != 0)
		    width = atoi(text);
	    }
	}
	if (tok == GT)
	    break;
    }

    width = (state.width * width) / 100;
    if (width > 200)
	width = 200;
    memset(hr, '-', width);
    hr[width] = 0;
    Save(sv);
    breakline();
    state.align = CENTER;
    addword(hr);
    breakline();
    Restore(sv);
} /* do_hr */


/*
 * eattag() gobbles up the rest of a tag that we don't care about
 */
void
eattag(FILE *input)
{
    int tok;

    while ((tok = scan(input)) != GT && tok != YYEOF)
	;
}


/*
 * parse_it() handles a section of html code,
 */
void
parse_it(FILE *input, int endtag, int level, unsigned long allowed_tags)
{
    int tok;

    allowed_tags |= BIT(BANGDASHDASH)|BIT(GT);

    while ((tok=scan(input)) != YYEOF) {
	if (tok == LT) {
	    tok = scan(input);

	    if (tok == endtag && (endtag != -HEADER || level == state.header_type)) {
		eattag(input);
		return;
	    }

	    if (tok < 0) {
		if ((BIT(-tok) & allowed_tags) == 0) {
		    eattag(input);
		    continue;
		}
	    }
	    else if (tok > 0) {
		if ((BIT(tok) & allowed_tags) == 0) {
		    eattag(input);
		    continue;
		}
	    }

	    switch (tok) {
	    case BANGDASHDASH:
		while ((tok=scan(input)) != YYEOF)
		    if (tok == DASHDASH)
			if ((tok = scan(input)) == GT || tok == YYEOF)
			    break;
		break;
	    case HTML:
		do_html(input);
		break;
	    case HEAD:
		do_head(input);
		break;
	    case DL:
		do_list(input);
		break;
	    case DT:
		do_bullet(input);
		eattag(input);
		break;
	    case DD:
		do_text(input);
		eattag(input);
		break;
	    case BODY:
		do_body(input);
		break;
	    case CENTER:
	    case PARA:
		do_paragraph(input, tok);
		break;
	    case HEADER:
		do_header(input, state.header_type);
		break;
	    case BREAK:
		newline();
		eattag(input);
		break;
	    case TITLE:
		do_title(input);
		break;
	    case A:
		do_a(input);
		break;
	    case PRE:
		do_pre(input);
		break;
	    case HR:
		do_hr(input);
		break;
	    case BOLD:
		set_bold();
		eattag(input);
		break;
	    case -BOLD:
		clear_bold();
		eattag(input);
		break;
	    case BQ:
		do_bq(input);
		break;
	    case ITAL:
		set_italic();
		eattag(input);
		break;
	    case -ITAL:
		clear_italic();
		eattag(input);
		break;
	    case TT:
		set_tt();
		eattag(input);
		break;
	    case -TT:
		clear_tt();
		eattag(input);
		break;
	    case GT:
		/* end of tag; could it be a <> tag? */
		unscan(tok, input);
		eattag(input);
		break;
	    default:
		eattag(input);
		break;
	    }
	}
	else {
	    if (tok == SPACE)
		addspace(text);
	    else
		addword(text);
	}
    }
} /* parse_it */
