/* html.c
 * Parse html tags.
 * This file is part of the edbrowse project, released under GPL.
 */

#include "eb.h"

#ifdef _MSC_VER			// sleep(secs) macro
#define SLEEP(a) Sleep(a * 1000)
extern int gettimeofday(struct timeval *tp, void *tzp);	// from tidys.lib
#else // !_MSC_VER
#define SLEEP sleep
#endif // _MSC_VER y/n

// OSX has no pthread_tryjoin_np, so we can't do our
// asynchronous timers under OSX, which is really no great loss.
#if defined(__APPLE__) || defined(__ANDROID__) || defined(__FreeBSD__)
#define pthread_tryjoin_np pthread_join
#endif

uchar browseLocal;
bool showHover, doColors;

static jsobjtype js_reset, js_submit;
static const int asyncTimer = 250;

bool tagHandler(int seqno, const char *name)
{
	Tag *t = tagList[seqno];
/* check the htnl tag attributes first */
	if (t->onclick && stringEqual(name, "onclick"))
		return true;
	if (t->onsubmit && stringEqual(name, "onsubmit"))
		return true;
	if (t->onreset && stringEqual(name, "onreset"))
		return true;
	if (t->onchange && stringEqual(name, "onchange"))
		return true;

	if (!t->jv)
		return false;
	if (!isJSAlive)
		return false;
	if (!handlerPresent(cf, t->jv, name))
		return false;

	if (stringEqual(name, "onclick"))
		t->onclick = true;
	if (stringEqual(name, "onsubmit"))
		t->onsubmit = true;
	if (stringEqual(name, "onreset"))
		t->onreset = true;
	if (stringEqual(name, "onchange"))
		t->onchange = true;
	return true;
}				/* tagHandler */

static void formReset(const Tag *form);

/*********************************************************************
This function was originally written to incorporate any strings generated by
document.write(), and it still does that,
but now it does much more.
It handles any side effects that occur from running js.
innerHTML tags generated, form input values set, timers,
form.reset(), form.submit(), document.location = url, etc.
Every js activity should start with jSyncup() and end with jSideEffects().
WARNING: this routine mucks with cf, so you need to set it afterwards,
the button being pushed or the onclick code or whatever frame is appropriate.
*********************************************************************/

void jSideEffects(void)
{
	if (!cw->browseMode || !isJSAlive)
		return;
	debugPrint(4, "jSideEffects starts");
	runScriptsPending(false);
	cw->mustrender = true;
	rebuildSelectors();
	debugPrint(4, "jSideEffects ends");
}				/* jSideEffects */

static Tag *locateOptionByName(const Tag *sel,
					  const char *name, int *pmc,
					  bool exact)
{
	Tag *t, *em = 0, *pm = 0;
	int pmcount = 0;	/* partial match count */
	const char *s;

	for (t = cw->optlist; t; t = t->same) {
		if (t->controller != sel)
			continue;
		if (!(s = t->textval))
			continue;
		if (stringEqualCI(s, name)) {
			em = t;
			continue;
		}
		if (exact)
			continue;
		if (strstrCI(s, name)) {
			pm = t;
			++pmcount;
		}
	}
	if (em)
		return em;
	if (pmcount == 1)
		return pm;
	*pmc = (pmcount > 0);
	return 0;
}				/* locateOptionByName */

static Tag *locateOptionByNum(const Tag *sel, int n)
{
	Tag *t;
	int cnt = 0;

	for (t = cw->optlist; t; t = t->same) {
		if (t->controller != sel)
			continue;
		if (!t->textval)
			continue;
		++cnt;
		if (cnt == n)
			return t;
	}
	return 0;
}				/* locateOptionByNum */

static bool
locateOptions(const Tag *sel, const char *input,
	      char **disp_p, char **val_p, bool setcheck)
{
	Tag *t;
	char *disp, *val;
	int disp_l, val_l;
	int len = strlen(input);
	int n, pmc;
	const char *s, *e;	/* start and end of an option */
	char *iopt;		/* individual option */
	Frame *f = sel->f0;

	iopt = (char *)allocMem(len + 1);
	disp = initString(&disp_l);
	val = initString(&val_l);

	if (setcheck) {
/* Uncheck all existing options, then check the ones selected. */
		if (sel->jv && allowJS)
			set_property_number(f, sel->jv, "selectedIndex", -1);
		for (t = cw->optlist; t; t = t->same) {
			if (t->controller == sel && t->textval) {
				t->checked = false;
				if (t->jv && allowJS)
					set_property_bool(f, t->jv, "selected",   false);
			}
		}
	}

	s = input;
	while (*s) {
		e = 0;
		if (sel->multiple)
			e = strchr(s, ',');
		if (!e)
			e = s + strlen(s);
		len = e - s;
		strncpy(iopt, s, len);
		iopt[len] = 0;
		s = e;
		if (*s == ',')
			++s;

		t = locateOptionByName(sel, iopt, &pmc, true);
		if (!t) {
			n = stringIsNum(iopt);
			if (n >= 0)
				t = locateOptionByNum(sel, n);
		}
		if (!t)
			t = locateOptionByName(sel, iopt, &pmc, false);
		if (!t) {
			if (n >= 0)
				setError(MSG_XOutOfRange, n);
			else
				setError(pmc + MSG_OptMatchNone, iopt);
/* This should never happen when we're doing a set check */
			if (setcheck) {
				runningError(MSG_OptionSync, iopt);
				continue;
			}
			goto fail;
		}

		if (val_p) {
			if (*val)
				stringAndChar(&val, &val_l, '\1');
			stringAndString(&val, &val_l, t->value);
		}

		if (disp_p) {
			if (*disp)
				stringAndChar(&disp, &disp_l, ',');
			stringAndString(&disp, &disp_l, t->textval);
		}

		if (setcheck) {
			t->checked = true;
			if (t->jv && allowJS) {
				set_property_bool(f, t->jv, "selected", true);
				if (sel->jv && allowJS)
					set_property_number(f, sel->jv,
							    "selectedIndex",
							    t->lic);
			}
		}
	}			/* loop over multiple options */

	if (val_p)
		*val_p = val;
	if (disp_p)
		*disp_p = disp;
	free(iopt);
	return true;

fail:
	free(iopt);
	nzFree(val);
	nzFree(disp);
	if (val_p)
		*val_p = 0;
	if (disp_p)
		*disp_p = 0;
	return false;
}				/* locateOptions */

/*********************************************************************
Sync up the javascript variables with the input fields.
This is required before running any javascript, e.g. an onclick function.
After all, the input fields may have changed.
You may have changed the last name from Flintstone to Rubble.
This has to propagate down to the javascript strings in the DOM.
This is quick and stupid; I just update everything.
Most of the time I'm setting the strings to what they were before;
that's the way it goes.
*********************************************************************/

void jSyncup(bool fromtimer)
{
	Tag *t;
	int itype, j, cx;
	char *value, *cxbuf;
	Frame *f;

	if (!cw->browseMode)
		return;		/* not necessary */
	if (cw->sank)
		return;		/* already done */
	cw->sank = true;
	if (!isJSAlive)
		return;
	debugPrint(4, "jSyncup starts");
	if (!fromtimer)
		cw->nextrender = 0;

	for (t = cw->inputlist; t; t = t->same) {
		itype = t->itype;
		if (itype <= INP_HIDDEN)
			continue;

/*********************************************************************
You could change input fields in several frames, and each item should be
passed down to its corresponding js context.
This line sets the frame, then we're ready to roll.
*********************************************************************/
		f = t->f0;

		if (itype >= INP_RADIO) {
			int checked = fieldIsChecked(t->seqno);
			if (checked < 0)
				continue;
			t->checked = checked;
			set_property_bool(f, t->jv, "checked", checked);
			continue;
		}

		value = getFieldFromBuffer(t->seqno);
/* If that line has been deleted from the user's buffer,
 * indicated by value = 0,
 * then don't do anything. */
		if (!value)
			continue;

		if (itype == INP_SELECT) {
/* set option.selected in js based on the option(s) in value */
			locateOptions(t, (value ? value : t->value), 0, 0,
				      true);
			if (value) {
				nzFree(t->value);
				t->value = value;
			}
			continue;
		}

		if (itype == INP_TA) {
			if (!value) {
				set_property_string(f, t->jv, "value", 0);
				continue;
			}
/* Now value is just <buffer 3>, which is meaningless. */
			nzFree(value);
			cx = t->lic;
			if (!cx)
				continue;
// unfoldBuffer could fail if we have quit that session.
			if (!unfoldBuffer(cx, false, &cxbuf, &j))
				continue;
			set_property_string(f, t->jv, "value", cxbuf);
			nzFree(cxbuf);
			continue;
		}

		if (value) {
			set_property_string(f, t->jv, "value", value);
			nzFree(t->value);
			t->value = value;
		}
	}			/* loop over tags */

	debugPrint(4, "jSyncup ends");
}				/* jSyncup */

void jClearSync(void)
{
	if (cw->browseMode) {
		cw->sank = false;
		return;
	}
/* when we are able to jSyncup windows other than the foreground window,
 * which we can't do yet, then the rest of this will make sense. */
#if 0
	for (cx = 1; cx <= maxSession; ++cx) {
		w = sessionList[cx].lw;
		while (w) {
			w->sank = false;
			w = w->prev;
		}
	}
#endif
}				/* jClearSync */

/*********************************************************************
This function is called for a new web page, by http refresh,
or by document.location = new_url, or by new Window().
If delay is 0 or less then the action should happen now.
The refresh parameter means replace the current page.
This is false only if js creates a new window, which should stack up on top of the old.
*********************************************************************/

char *newlocation;
int newloc_d;			/* possible delay */
bool newloc_r;			/* replace the buffer */
Frame *newloc_f;	/* frame calling for new web page */
bool js_redirects;
static void gotoLocation(char *url, int delay, bool rf)
{
	if (!allowRedirection) {
		debugPrint(1, "javascript redirection disabled: %s", url);
		nzFree(url);
		return;
	}
	if (newlocation && delay >= newloc_d) {
		nzFree(url);
		return;
	}
	nzFree(newlocation);
	newlocation = url;
	newloc_d = delay;
	newloc_r = rf;
	newloc_f = cf;
	if (!delay)
		js_redirects = true;
}				/* gotoLocation */

/* helper function for meta tag */
void htmlMetaHelper(Tag *t)
{
	char *name;
	const char *content, *heq;
	char **ptr;
	char *copy = 0;

/* if we're generating a cookie, we better get the frame right,
 * because that's the url that the cookie belongs to.
 * I think the frame is correct anyways, because we are parsing html,
 * but just to be safe ... */
	cf = t->f0;

	name = t->name;
	content = attribVal(t, "content");
	copy = cloneString(content);
	heq = attribVal(t, "http-equiv");

	if (heq && content) {
		bool rc;
		int delay;

/* It's not clear if we should process the http refresh command
 * immediately, the moment we spot it, or if we finish parsing
 * all the html first.
 * Does it matter?  It might.
 * A subsequent meta tag could use http-equiv to set a cooky,
 * and we won't see that cooky if we jump to the new page right now.
 * And there's no telling what subsequent javascript might do.
 * So I'm going to postpone the refresh until everything is parsed.
 * Bear in mind, we really don't want to refresh if we're working
 * on a local file. */

		if (stringEqualCI(heq, "Set-Cookie")) {
			rc = receiveCookie(cf->fileName, content);
			debugPrint(3, rc ? "jar" : "rejected");
		}

		if (allowRedirection && !browseLocal
		    && stringEqualCI(heq, "Refresh")) {
			if (parseRefresh(copy, &delay)) {
				char *newcontent;
				unpercentURL(copy);
				newcontent = resolveURL(cf->hbase, copy);
				gotoLocation(newcontent, delay, true);
			}
		}
	}

	if (name) {
		ptr = 0;
		if (stringEqualCI(name, "description"))
			ptr = &cw->htmldesc;
		if (stringEqualCI(name, "keywords"))
			ptr = &cw->htmlkey;
		if (ptr && !*ptr && content) {
			stripWhite(copy);
			*ptr = copy;
			copy = 0;
		}
	}

	nzFree(copy);
}				/* htmlMetaHelper */

/* pre is the predecoration from edbrowse-js, if appropriate */
static void runGeneratedHtml(Tag *t, const char *h)
{
	int l = cw->numTags;

	if (t)
		debugPrint(4, "parse under %s %d", t->info->name, t->seqno);
	else
		debugPrint(4, "parse under top");
	debugPrint(4, "Generated {%s}", h);

	html2nodes(h, false);
	htmlGenerated = true;
	htmlNodesIntoTree(l, t);
	prerender(false);
	decorate(0);
}				/* runGeneratedHtml */

/* helper function to prepare an html script.
 * steps: 1 parsed as html, 2 decorated with a coresponding javascript object
 * 3 downloading in background (not yet implemented),
 * 4 data fetched and in the js world and possibly deminimized,
 * 5 script has run, 6 script could not run. */

void prepareScript(Tag *t)
{
	const char *js_file = "generated";
	char *js_text = 0;
	const char *filepart;
	char *b;
	int blen;
	Frame *f = t->f0;

	if (intFlag)
		goto fail;

/* As per the starting line number, we cant distinguish between
 * <script> foo </script>  and
 * <script>
 * foo
 * </script>
 * so make a guess towards the first form, knowing we could be off by 1.
 * Just leave it at t->js_ln */
	if (f->fileName && !t->scriptgen)
		js_file = f->fileName;

	if (t->jv) {
// js might have set, or changed, the source url.
		char *new_url = get_property_url(f, t->jv, false);
		if (new_url && *new_url) {
			if (t->href && !stringEqual(t->href, new_url))
				debugPrint(3, "js replaces script %s with %s",
					   t->href, new_url);
			nzFree(t->href);
			t->href = new_url;
		}
		t->async = get_property_bool(f, t->jv, "async");
// A side effect of tidy + edbrowse is that the text of the script is a
// childNode of script, but I don't think it should be.
		if (t->firstchild && t->firstchild->action == TAGACT_TEXT)
			run_function_onearg(f, t->jv, "removeChild",
					    t->firstchild->jv);
	}

	if (t->href) {		/* fetch the javascript page */
		const char *altsource = 0;
		bool from_data;
		if (!javaOK(t->href))
			goto fail;
		from_data = isDataURI(t->href);
		if (!from_data) {
			altsource = fetchReplace(t->href);
			if (!altsource)
				altsource = t->href;
		}
		debugPrint(3, "js source %s",
			   !from_data ? altsource : "data URI");
		if (from_data) {
			char *mediatype;
			int data_l = 0;
			if (parseDataURI(t->href, &mediatype,
					 &js_text, &data_l)) {
				prepareForBrowse(js_text, data_l);
				nzFree(mediatype);
			} else {
				debugPrint(3,
					   "Unable to parse data URI containing JavaScript");
				goto fail;
			}
		} else if (browseLocal && !isURL(altsource)) {
			char *h = cloneString(altsource);
			unpercentString(h);
			if (!fileIntoMemory(h, &b, &blen)) {
				if (debugLevel >= 1)
					i_printf(MSG_GetLocalJS);
				nzFree(h);
				goto fail;
			}
			js_text = force_utf8(b, blen);
			if (!js_text)
				js_text = b;
			else
				nzFree(b);
			nzFree(h);
		} else {
			struct i_get g;

// this has to happen before threads spin off
			if (!curlActive) {
				eb_curl_global_init();
				cookiesFromJar();
				setupEdbrowseCache();
			}

			if (down_jsbg && !demin && !uvw
			    && !pthread_create(&t->loadthread, NULL,
					       httpConnectBack2, (void *)t)) {
				t->js_ln = 1;
				js_file = altsource;
				filepart = getFileURL(js_file, true);
				t->js_file = cloneString(filepart);
// stop here and wait for the child process to download
				t->step = 3;
				return;
			}
			memset(&g, 0, sizeof(g));
			g.thisfile = f->fileName;
			g.uriEncoded = true;
			g.url = t->href;
			if (!httpConnect(&g)) {
				if (debugLevel >= 3)
					i_printf(MSG_GetJS2);
				goto fail;
			}
			if (g.code == 200) {
				js_text = force_utf8(g.buffer, g.length);
				if (!js_text)
					js_text = g.buffer;
				else
					nzFree(g.buffer);
			} else {
				nzFree(g.buffer);
				if (debugLevel >= 3)
					i_printf(MSG_GetJS, g.url, g.code);
			}
		}
		t->js_ln = 1;
		js_file = (!from_data ? altsource : "data_URI");
	} else {
		js_text = t->textval;
		t->textval = 0;
	}

	if (!js_text) {
// we can only run if javascript has supplied the code forr this scrip,
// because we have none to offer.
// Such code cannot be deminimized.
		goto success;
	}
	set_property_string(f, t->jv, "text", js_text);
	nzFree(js_text);

	filepart = getFileURL(js_file, true);
	t->js_file = cloneString(filepart);

// deminimize the code if we're debugging.
	if (demin)
		run_function_onearg(f, f->winobj, "eb$demin", t->jv);
	if (uvw)
		run_function_onearg(f, f->winobj, "eb$watch", t->jv);

success:
	t->step = 4;
	return;

fail:
	t->step = 6;
}				/* prepareScript */

static bool is_subframe(Frame *f1, Frame *f2)
{
	Tag *t;
	int n;
	if (f1 == f2)
		return true;
	while (true) {
		for (n = 0; n < cw->numTags; ++n) {
			t = tagList[n];
			if (t->f1 == f1)
				goto found;
		}
		return false;
found:
		f1 = t->f0;
		if (f1 == f2)
			return true;
	}
}

/*********************************************************************
Run pending scripts, and perform other actions that have been queued up by javascript.
This includes document.write, linkages, perhaps even form.submit.
Things stop however if we detect document.location = new_url,
i.e. a page replacement, as indicated by the newlocation variable being set.
The old page doesn't matter any more.
I run the scripts linked to the current frame.
That way the scripts in a subframe will run, then return, then the scripts
in the parent frame pick up where they left off.
The algorithm for which sripts to run when is far from obvious,
and nowhere documented.
I had to write several contrived web pages and run them through chrome
and firefox and document the results.

1. Scripts that come from the internet (src=url) are different from
inline scripts, i.e. those that are part of the home page, or generated
dynamically with s.text set.
An internet script is loaded, that is, fetched from the internet and then run,
and after that, it's onload handler is run.
An inline script does not run its onload handler, even if it has one.

2. <style> is inline, but <link href=url rel=stylesheet> is internet.
As above, onload code is run after an internet css page is fetched.
This is rather asynchronous, relative to the other scripts.
Just run the onload code when you can.

3. All the scripts on the home page run in sequence.
Each internet script must fetch and run before the next script runs.
Of course all the internet scripts can download in parallel, to save time,
and I do that if down_jsbg is true,
but still, we have to execute them in order.

4. A script could have async set, and in theory I could
skip that one and do the next one if it is available (postpone), or even do the
async script in another thread, but I can't, because duktape is not threadsafe,
as is clearly documented.
So I allow for postponement, that is, two passes,
the first pass runs scripts in order and skips async scripts,
the second pass runs the async scripts.
Pass 2 runs the async scripts in order, and it doesn't have to, but it's
the easiest way to go, and how often do we have several async scripts,
some ready several seconds before others? Not very often.

5. These scripts can generate other scripts, which run in the next wave.
However, if the generated script is inline, not from the internet,
it runs right now.
The first script pauses, runs the second script, then resumes.
I demonstrated this in my contrived web page,
but not sure it ever happens in the real world.
If your second script sets s.text = "some code here",
then why not embed that code in the first script and be done with it?
So I haven't gone to the bother of implementing this.
All generated scripts, inline and internet, run later.
But if we wanted to implement this, you can probably follow the pattern
set by URL and several other classes.
Create a member text$2, that's where the code lives.
A getter returns text$2 when you ask for text.
A setter runs the code through eval(),
then stores it in text$2 for future reference.
The inline script is executed now.
All the C code deals with text$2 so there are no unintended side effects.
So it's not too hard I suppose, but I haven't seen the need yet.

6. DOMContentLoaded is a strange event.
It fires when the first wave of scripts is complete, but before the second wave or any onload handlers etc.
The startbrowse argument tells runScriptsPending() we are calling it
because a new page is being browsed.
It should dispatch DOMContentLoaded after the first scripts have run.

7. Generated scripts do not run unless and until they are connected to the tree.
If they are connected later, that is when they run.
This is true of both inline and internet.
The first is problematic, because my implementation, in 5,
that just calls eval(s.text) on a setter,
would run all the time, whether the new script was connected or not.
So my 5 implementation would fix one problem and create another.
Not sure what to do about that.

8. Scripts are run in creation order, not in tree order.
Or maybe the order is arbitrary and not prescribed,
in which case creation order is fine.
That is easiest, and it's what I do.
*********************************************************************/

void runScriptsPending(bool startbrowse)
{
	Tag *t, *up;
	char *js_file;
	const char *a;
	int ln;
	bool change, async;
	jsobjtype v;
	Frame *f, *save_cf = cf;

	if (newlocation && newloc_r)
		return;

// Not sure where document.write objects belong.
// For now I'm putting them under body.
// Each write corresponds to the frame containing document.write.
	for (f = &(cw->f0); f; f = f->next) {
		if (!f->dw)
			continue;
		cf = f;
		stringAndString(&cf->dw, &cf->dw_l, "</body>\n");
		runGeneratedHtml(cf->bodytag, cf->dw);
		nzFree(cf->dw);
		cf->dw = 0;
		cf->dw_l = 0;
	}

top:
	change = false;

	for (t = cw->scriptlist; t; t = t->same) {
		if (t->dead || !t->jv || t->step >= 3)
			continue;

/*********************************************************************
Scripts do not run unless connected to the tree; see point 7
in the earlier comments.
Climb up until we reach HEAD or BODY.
If neither of these, then don't run the script, and in fact it's probably
unsafe to prepare it.
1. Maybe it isn't ready, which is why it isn't linked into the tree.
Maybe script.src is still being constructed.
We should skip it and try again later.
2. Maybe it was just an exercise, and will never be used.
We move into prepareScript(), and set some property of the script object.
This trigggers garbage collection, and the object goes away.
Our tag is marked dead in response, but we don't check for that in every step
of preparation and execution.
The next time we try to use this object in any way, it blows up.
So it is best to skate past a script that is not linked into the tree.
We skip past it here, and it isn't prepared, so it won't execute later.
*********************************************************************/
		for(up = t; up; up = up->parent)
			if(up->action == TAGACT_HEAD || up->action == TAGACT_BODY)
				break;
		if (!up)
			continue;

		cf = t->f0;
		prepareScript(t);
// step will now be 3, load in background, 4, loaded, or 6, failure.
	}

	async = false;
passes:

	for (t = cw->scriptlist; t; t = t->same) {
		if (t->dead || !t->jv || t->step >= 5 || t->step <= 2 || t->async != async)
			continue;
		cf = t->f0;
		if (!is_subframe(cf, save_cf))
			continue;
		if (intFlag) {
			t->step = 6;
			continue;
		}

		if (async && asyncTimer && down_jsbg && cw->browseMode) {
			if (!t->intimer) {
				scriptSetsTimeout(t);
				t->intimer = true;
			}
			continue;
		}

		if (t->step == 3) {
// waiting for background process to load
			pthread_join(t->loadthread, NULL);
			if (!t->loadsuccess) {
				if (debugLevel >= 3)
					i_printf(MSG_GetJS, t->href, t->hcode);
				t->step = 6;
				continue;
			}
			set_property_string(cf, t->jv,
					    (t->inxhr ? "$entire" : "text"),
					    t->value);
			nzFree(t->value);
			t->value = 0;
			t->step = 4;	// loaded
		}

		t->step = 5;	// now running the script

		if (t->inxhr) {
// xhr looks like an asynchronous script before browse
			char *gc_name;
			jsobjtype xo = t->jv;	// xhr object
			run_function_bool(cf, xo, "parseResponse");
/*********************************************************************
Ok this is subtle. I put it on a script tag, and t.jv.onload exists!
That is the function that is run by xhr.
So runOnload() comes along and runs it again, unless we do something.
I will disconnect here, and also check for inxhr in runOnload().
*********************************************************************/
			disconnectTagObject(t);
			t->dead = true;
// allow garbage collection to recapture xo if it wants to
			gc_name = get_property_string(cf, xo, "backlink");
			if (gc_name)
				delete_property(cf, cf->winobj, gc_name);
			goto afterscript;
		}

// If no language is specified, javascript is default.
		a = get_property_string(cf, t->jv, "language");
		if (a && *a && (!memEqualCI(a, "javascript", 10) || isalphaByte(a[10]))) {
			debugPrint(3, "script tag %d language %s not executed", t->seqno, a);
			cnzFree(a);
			goto afterscript;
		}
		cnzFree(a);
// Also reject a script if a type is specified and it is not JS.
// For instance, some JSON pairs in script tags on amazon.com
		a = get_property_string(cf, t->jv, "type");
		if (a && *a && (!memEqualCI(a, "javascript", 10))
		    && (!memEqualCI(a, "text/javascript", 15))) {
			debugPrint(3, "script tag %d type %s not executed", t->seqno, a);
			cnzFree(a);
			goto afterscript;
		}
		cnzFree(a);

		js_file = t->js_file;
		if (!js_file)
			js_file = "generated";
		if (cf != save_cf)
			debugPrint(4, "running script at a lower frame %s",
				   js_file);
		ln = t->js_ln;
		if (!ln)
			ln = 1;
		debugPrint(3, "exec %s at %d", js_file, ln);
/* if script is in the html it usually begins on the next line, so increment,
 * and hope the error messages line up. */
		if (ln > 1)
			++ln;
		set_property_object(cf, cf->docobj, "currentScript", t->jv);
		jsRunData(cf, t->jv, js_file, ln);

// I don't know when to run the onload function, before or after the script.
// And should we run it for a json script or other non js script?
// If so that's a problem, cause we already jumped ahead to afterscript:

		if (t->js_file && !isDataURI(t->href) && handlerPresent(cf, t->jv, "onload"))
			run_event_bool(cf, t->jv, "script", "onload");
		delete_property(cf, cf->docobj, "currentScript");
		debugPrint(3, "exec complete");

afterscript:
		if (newlocation && newloc_r) {
			cf = save_cf;
			return;
		}

/* look for document.write from this script */
		if (cf->dw) {
			stringAndString(&cf->dw, &cf->dw_l, "</body>\n");
			runGeneratedHtml(t, cf->dw);
			nzFree(cf->dw);
			cf->dw = 0;
			cf->dw_l = 0;
			run_function_onearg(cf, cf->winobj, "eb$uplift", t->jv);
		}

		change = true;
	}

// after each pass, see if there is a link onload to run.
	for (t = cw->linklist; t; t = t->same) {
		if(t->lic == 1 && t->jv && !t->dead) {
			if(handlerPresent(t->f0, t->jv, "onload")) {
				run_event_bool(t->f0, t->jv, "link", "onload");
				change = true;
			}
t->lic = 0;
		}
	}

	if (!async) {
		if(startbrowse)
// I think it's ok to use cf here, but let's be safe.
			run_event_bool(save_cf, save_cf->docobj, "document", "onDOMContentLoaded");
		startbrowse = false;
		async = true;
		goto passes;
	}

	if (change)
		goto top;

	if ((v = js_reset)) {
		js_reset = 0;
		if ((t = tagFromJavaVar(v)))
			formReset(t);
	}

	if ((v = js_submit)) {
		js_submit = 0;
		if ((t = tagFromJavaVar(v))) {
			char *post;
			bool rc = infPush(t->seqno, &post);
			if (rc)
				gotoLocation(post, 0, false);
			else
				showError();
		}
	}

	cf = save_cf;
}				/* runScriptsPending */

void preFormatCheck(int tagno, bool * pretag, bool * slash)
{
	const Tag *t;
	*pretag = *slash = false;
	if (tagno >= 0 && tagno < cw->numTags) {
		t = tagList[tagno];
		*pretag = (t->action == TAGACT_PRE);
		*slash = t->slash;
	}
}				/* preFormatCheck */

/* is there a doorway from html to js? */
static bool jsDoorway(void)
{
	const Tag *t;
	int j;
	for (j = 0; j < cw->numTags; ++j) {
		t = tagList[j];
		if (t->doorway)
			return true;
	}
	debugPrint(3, "no js doorway");
	return false;
}				/* jsDoorway */

char *htmlParse(char *buf, int remote)
{
	char *a, *newbuf;

	if (tagList)
		i_printfExit(MSG_HtmlNotreentrant);
	if (remote >= 0)
		browseLocal = !remote;
	initTagArray();
	cf->baseset = false;
	cf->hbase = cloneString(cf->fileName);

/* call the tidy parser to build the html nodes */
	html2nodes(buf, true);
	nzFree(buf);
	htmlGenerated = false;
	htmlNodesIntoTree(0, NULL);
	prerender(false);

/* if the html doesn't use javascript, then there's
 * no point in generating it.
 * This is typical of generated html, from pdf for instance,
 * or the html that is in email. */
	if (cf->cx && !jsDoorway())
		freeJavaContext(cf);

	if (isJSAlive) {
// the "create handlers" messages aren't helpful here.
		if (debugEvent && debugLevel >= 3)
			set_property_bool(cf, cf->winobj, "eventDebug", false);
		decorate(0);
		if (debugEvent && debugLevel >= 3)
			set_property_bool(cf, cf->winobj, "eventDebug", true);
		set_basehref(cf->hbase);
		run_function_bool(cf, cf->winobj, "eb$qs$start");
		runScriptsPending(true);
		runOnload();
		runScriptsPending(false);

/*********************************************************************
		set_property_string(cf->docobj, "readyState", "complete");
This should work, and does almost every time, except for
https://www.digicatapult.org.uk/
which calls https://ajax.cloudflare.com/cdn-cgi/scripts/7089c43e/cloudflare-static/rocket-loader.min.js
which replaces readyState with a getter and no setter.
If we try to set it using this unprotected duktape call it aborts.
I could add protection around set_property_string, or write a protected version
thereof, and some day I might have to do that,
but this is called only once per browse,
so no impact on performance if we invoke a script,
so for now I'm taking the easy way out.
jsRunScript is protected.
*********************************************************************/
		jsRunScript(cf, cf->winobj, "document.readyState='complete'",
			    "readyState", 1);

		run_event_bool(cf, cf->docobj, "document", "onreadystatechange");
		run_event_bool(cf, cf->winobj, "window", "onfocus");
		run_event_bool(cf, cf->docobj, "document", "onfocus");

		runScriptsPending(false);
		rebuildSelectors();
	}

	a = render(0);
	debugPrint(6, "|%s|\n", a);
	newbuf = htmlReformat(a);
	nzFree(a);

	return newbuf;
}				/* htmlParse */

/* See if there are simple tags like <p> or </font> */
bool htmlTest(void)
{
	int j, ln;
	int cnt = 0;
	int fsize = 0;		/* file size */
	char look[12];
	bool firstline = true;

	for (ln = 1; ln <= cw->dol; ++ln) {
		char *p = (char *)fetchLine(ln, -1);
		char c;
		int state = 0;

		while (isspaceByte(*p) && *p != '\n')
			++p;
		if (*p == '\n')
			continue;	/* skip blank line */
		if (firstline && *p == '<') {
/* check for <!doctype and other things */
			if (memEqualCI(p + 1, "!doctype", 8))
				return true;
			if (memEqualCI(p + 1, "?xml", 4))
				return true;
			if (memEqualCI(p + 1, "!--", 3))
				return true;
/* If it starts with <tag, for any tag we recognize,
 * we'll call it good. */
			for (j = 1; j < 10; ++j) {
				if (!isalnumByte(p[j]))
					break;
				look[j - 1] = p[j];
			}
			look[j - 1] = 0;
			if (j > 1 && (p[j] == '>' || isspaceByte(p[j]))) {
/* something we recognize? */
				const struct tagInfo *ti;
				for (ti = availableTags; ti->name[0]; ++ti)
					if (stringEqualCI(ti->name, look))
						return true;
			}	/* leading tag */
		}		/* leading < */
		firstline = false;

/* count tags through the buffer */
		for (j = 0; (c = p[j]) != '\n'; ++j) {
			if (state == 0) {
				if (c == '<')
					state = 1;
				continue;
			}
			if (state == 1) {
				if (c == '/')
					state = 2;
				else if (isalphaByte(c))
					state = 3;
				else
					state = 0;
				continue;
			}
			if (state == 2) {
				if (isalphaByte(c))
					state = 3;
				else
					state = 0;
				continue;
			}
			if (isalphaByte(c))
				continue;
			if (c == '>')
				++cnt;
			state = 0;
		}
		fsize += j;
	}			/* loop over lines */

/* we need at least one of these tags every 300 characters.
 * And we need at least 4 such tags.
 * Remember, you can always override by putting <html> at the top. */
	return (cnt >= 4 && cnt * 300 >= fsize);
}				/* htmlTest */

/* Show an input field */
void infShow(int tagno, const char *search)
{
	const Tag *t = tagList[tagno], *v;
	const char *s;
	int cnt;
	bool show;

	s = inp_types[t->itype];
	if (*s == ' ')
		++s;
	printf("%s", s);
	if (t->multiple)
		i_printf(MSG_Many);
	if (t->itype == INP_TEXT && t->lic)
		printf("[%d]", t->lic);
	if (t->itype_minor != INP_NO_MINOR)
		printf(" (%s)", inp_others[t->itype_minor]);
	if (t->itype == INP_TA) {
		const char *rows = attribVal(t, "rows");
		const char *cols = attribVal(t, "cols");
		const char *wrap = attribVal(t, "wrap");
		if (rows && cols) {
			printf("[%sx%s", rows, cols);
			if (wrap && stringEqualCI(wrap, "virtual"))
				i_printf(MSG_Recommended);
			i_printf(MSG_Close);
		}
	}			/* text area */
	if (t->name)
		printf(" %s", t->name);
	nl();
	if (t->itype != INP_SELECT)
		return;

/* display the options in a pick list */
/* If a search string is given, display the options containing that string. */
	cnt = 0;
	show = false;
	for (v = cw->optlist; v; v = v->same) {
		if (v->controller != t)
			continue;
		if (!v->textval)
			continue;
		++cnt;
		if (*search && !strstrCI(v->textval, search))
			continue;
		show = true;
		printf("%3d %s\n", cnt, v->textval);
	}
	if (!show) {
		if (!*search)
			i_puts(MSG_NoOptions);
		else
			i_printf(MSG_NoOptionsMatch, search);
	}
}				/* infShow */

static bool inputDisabled(const Tag *t)
{
	if (allowJS && t->jv)
		return get_property_bool(t->f0, t->jv, "disabled");
	return t->disabled;
}

/*********************************************************************
Update an input field in the current edbrowse buffer.
This can be done for one of two reasons.
First, the user has interactively entered a value in the form, such as
	i=foobar
In this case fromForm will be set to true.
I need to find the tag in the current buffer.
He just modified it, so it ought to be there.
If it isn't there, print an error and do nothing.
The second case: the value has been changed by form reset,
either the user has pushed the reset button or javascript has called form.reset.
Here fromForm is false.
I'm not sure why js would reset a form before the page was even rendered;
that's the only way the line should not be found,
or perhaps if that section of the web page was deleted.
notify = true causes the line to be printed after the change is made.
Notify true and fromForm false is impossible.
You don't need to be notified as each variable is changed during a reset.
The new line replaces the old, and the old is freed.
This works because undo is disabled in browse mode.
*********************************************************************/

static void
updateFieldInBuffer(int tagno, const char *newtext, bool notify, bool fromForm)
{
	int ln, n, plen;
	char *p, *s, *t, *new;

	if (locateTagInBuffer(tagno, &ln, &p, &s, &t)) {
		n = (plen = pstLength((pst) p)) + strlen(newtext) - (t - s);
		new = allocMem(n);
		memcpy(new, p, s - p);
		strcpy(new + (s - p), newtext);
		memcpy(new + strlen(new), t, plen - (t - p));
		free(cw->map[ln].text);
		cw->map[ln].text = (pst) new;
		if (notify)
			displayLine(ln);
		return;
	}

	if (fromForm)
		i_printf(MSG_NoTagFound, tagno, newtext);
}				/* updateFieldInBuffer */

/* Update an input field. */
bool infReplace(int tagno, const char *newtext, bool notify)
{
	const Tag *t = tagList[tagno], *v;
	const Tag *form = t->controller;
	char *display;
	int itype = t->itype;
	int itype_minor = t->itype_minor;
	int newlen = strlen(newtext);

/* sanity checks on the input */
	if (itype <= INP_SUBMIT) {
		int b = MSG_IsButton;
		if (itype == INP_SUBMIT || itype == INP_IMAGE)
			b = MSG_SubmitButton;
		if (itype == INP_RESET)
			b = MSG_ResetButton;
		setError(b);
		return false;
	}

	if (itype == INP_TA) {
		setError(MSG_Textarea, t->lic);
		return false;
	}

	if (t->rdonly) {
		setError(MSG_Readonly);
		return false;
	}
	if (inputDisabled(t)) {
		setError(MSG_Disabled);
		return false;
	}

	if (strchr(newtext, '\n')) {
		setError(MSG_InputNewline);
		return false;
	}

	if (itype >= INP_RADIO) {
		if ((newtext[0] != '+' && newtext[0] != '-') || newtext[1]) {
			setError(MSG_InputRadio);
			return false;
		}
		if (itype == INP_RADIO && newtext[0] == '-') {
			setError(MSG_ClearRadio);
			return false;
		}
	}

	if (itype == INP_SELECT) {
		if (!locateOptions(t, newtext, 0, 0, false))
			return false;
		locateOptions(t, newtext, &display, 0, false);
		updateFieldInBuffer(tagno, display, notify, true);
		nzFree(display);
	}

	if (itype == INP_FILE) {
		if (!envFile(newtext, &newtext))
			return false;
		if (newtext[0] && access(newtext, 4)) {
			setError(MSG_FileAccess, newtext);
			return false;
		}
	}

	if (itype == INP_TEXT && t->lic && newlen > t->lic) {
		setError(MSG_InputLong, t->lic);
		return false;
	}

	if (itype_minor == INP_NUMBER && (*newtext && stringIsNum(newtext) < 0)) {
		setError(MSG_NumberExpected);
		return false;
	}

	if (itype_minor == INP_EMAIL && (*newtext && !isEmailAddress(newtext))) {
		setError(MSG_EmailInput);
		return false;
	}

	if (itype_minor == INP_URL && (*newtext && !isURL(newtext))) {
		setError(MSG_UrlInput);
		return false;
	}

	if (itype == INP_RADIO && form && t->name && *newtext == '+') {
/* clear the other radio button */
		for (v = cw->inputlist; v; v = v->same) {
			if (v->controller != form)
				continue;
			if (v->itype != INP_RADIO)
				continue;
			if (!v->name)
				continue;
			if (!stringEqual(v->name, t->name))
				continue;
			if (fieldIsChecked(v->seqno) == true)
				updateFieldInBuffer(v->seqno, "-", false, true);
		}
	}

	if (itype != INP_SELECT) {
		updateFieldInBuffer(tagno, newtext, notify, true);
	}

	if (itype >= INP_TEXT) {
		jSyncup(false);
		cf = t->f0;
		if (itype >= INP_RADIO) {
// The change has already been made;
// if onclick returns false, should that have prevented the change??
			bubble_event(t, "onclick");
			if (js_redirects)
				return true;
		}
		if (itype != INP_SELECT)
			bubble_event(t, "oninput");
		if (js_redirects)
			return true;
		bubble_event(t, "onchange");
		if (js_redirects)
			return true;
		jSideEffects();
	}

	return true;
}				/* infReplace */

/*********************************************************************
Reset or submit a form.
This function could be called by javascript, as well as a human.
It must therefore update the js variables and the text simultaneously.
Most of this work is done by resetVar().
To reset a variable, copy its original value, in the html tag,
back to the text buffer, and over to javascript.
*********************************************************************/

static void resetVar(Tag *t)
{
	int itype = t->itype;
	Frame *f = t->f0;
	const char *w = t->rvalue;
	bool bval;

/* This is a kludge - option looks like INP_SELECT */
	if (t->action == TAGACT_OPTION)
		itype = INP_SELECT;

	if (itype <= INP_SUBMIT)
		return;

	if (itype >= INP_SELECT && itype != INP_TA) {
		bval = t->rchecked;
		t->checked = bval;
		w = bval ? "+" : "-";
	}

	if (itype == INP_TA) {
		int cx = t->lic;
		if (cx)
			sideBuffer(cx, w, -1, 0);
	} else if (itype != INP_HIDDEN && itype != INP_SELECT)
		updateFieldInBuffer(t->seqno, w, false, false);

	if ((itype >= INP_TEXT && itype <= INP_FILE) || itype == INP_TA) {
		nzFree(t->value);
		t->value = cloneString(t->rvalue);
	}

	if (!t->jv || !allowJS)
		return;

	if (itype >= INP_RADIO) {
		set_property_bool(f, t->jv, "checked", bval);
	} else if (itype == INP_SELECT) {
/* remember this means option */
		set_property_bool(f, t->jv, "selected", bval);
		if (bval && !t->controller->multiple && t->controller->jv)
			set_property_number(f, t->controller->jv,
					    "selectedIndex", t->lic);
	} else
		set_property_string(f, t->jv, "value", w);
}				/* resetVar */

static void formReset(const Tag *form)
{
	Tag *t;
	int i, itype;
	char *display;
	Frame *f = form->f0;

	rebuildSelectors();

	for (i = 0; i < cw->numTags; ++i) {
		t = tagList[i];
		if (t->action == TAGACT_OPTION) {
			resetVar(t);
			continue;
		}

		if (t->action != TAGACT_INPUT)
			continue;
		if (t->controller != form)
			continue;
		itype = t->itype;
		if (itype != INP_SELECT) {
			resetVar(t);
			continue;
		}
		if (t->jv && allowJS)
			set_property_number(f, t->jv, "selectedIndex", -1);
	}			/* loop over tags */

/* loop again to look for select, now that options are set */
	for (t = cw->inputlist; t; t = t->same) {
		if (t->controller != form)
			continue;
		itype = t->itype;
		if (itype != INP_SELECT)
			continue;
		display = displayOptions(t);
		updateFieldInBuffer(t->seqno, display, false, false);
		nzFree(t->value);
		t->value = display;
/* this should now be the same as t->rvalue, but I guess I'm
 * not going to check for that, or take advantage of it. */
	}			/* loop over tags */

	i_puts(MSG_FormReset);
}				/* formReset */

/* Fetch a field value (from a form) to post. */
/* The result is allocated */
static char *fetchTextVar(const Tag *t)
{
	char *v;

// js must not muck with the value of a file field
	if (t->itype != INP_FILE) {
		if (t->jv && allowJS)
			return get_property_string(t->f0, t->jv, "value");
	}

	if (t->itype > INP_HIDDEN) {
		v = getFieldFromBuffer(t->seqno);
		if (v)
			return v;
	}

/* Revert to the default value */
	return cloneString(t->value);
}				/* fetchTextVar */

static bool fetchBoolVar(const Tag *t)
{
	int checked;

	if (t->jv && isJSAlive)
		return get_property_bool(t->f0, t->jv,
					 (t->action ==
					  TAGACT_OPTION ? "selected" :
					  "checked"));

	checked = fieldIsChecked(t->seqno);
	if (checked < 0)
		checked = t->rchecked;
	return checked;
}				/* fetchBoolVar */

/* Some information on posting forms can be found here.
 * http://www.w3.org/TR/REC-html40/interact/forms.html */

static char *pfs;		/* post form string */
static int pfs_l;
static const char *boundary;

static void postDelimiter(char fsep)
{
	char c = pfs[strlen(pfs) - 1];
	if (c == '?' || c == '\1')
		return;
	if (fsep == '-') {
		stringAndString(&pfs, &pfs_l, "--");
		stringAndString(&pfs, &pfs_l, boundary);
		stringAndChar(&pfs, &pfs_l, '\r');
		fsep = '\n';
	}
	stringAndChar(&pfs, &pfs_l, fsep);
}				/* postDelimiter */

static bool
postNameVal(const char *name, const char *val, char fsep, uchar isfile)
{
	char *enc;
	const char *ct, *ce;	/* content type, content encoding */

	if (!name)
		name = emptyString;
	if (!val)
		val = emptyString;
	if (!*name && !*val)
		return true;

	postDelimiter(fsep);
	switch (fsep) {
	case '&':
		enc = encodePostData(name, NULL);
		stringAndString(&pfs, &pfs_l, enc);
		stringAndChar(&pfs, &pfs_l, '=');
		nzFree(enc);
		break;

	case '\n':
		stringAndString(&pfs, &pfs_l, name);
		stringAndString(&pfs, &pfs_l, "=\r\n");
		break;

	case '-':
		stringAndString(&pfs, &pfs_l,
				"Content-Disposition: form-data; name=\"");
		stringAndString(&pfs, &pfs_l, name);
		stringAndChar(&pfs, &pfs_l, '"');
/* I'm leaving nl off, in case we need ; filename */
		break;
	}			/* switch */

	if (!*val && fsep == '&')
		return true;

	switch (fsep) {
	case '&':
		enc = encodePostData(val, NULL);
		stringAndString(&pfs, &pfs_l, enc);
		nzFree(enc);
		break;

	case '\n':
		stringAndString(&pfs, &pfs_l, val);
		stringAndString(&pfs, &pfs_l, eol);
		break;

	case '-':
		if (isfile) {
			if (isfile & 2) {
				stringAndString(&pfs, &pfs_l, "; filename=\"");
				stringAndString(&pfs, &pfs_l, val);
				stringAndChar(&pfs, &pfs_l, '"');
			}
			if (!encodeAttachment(val, 0, true, &ct, &ce, &enc))
				return false;
			val = enc;
/* remember to free val in this case */
		} else {
			const char *s;
			ct = "text/plain";
/* Anything nonascii makes it 8bit */
			ce = "7bit";
			for (s = val; *s; ++s)
				if (*s < 0) {
					ce = "8bit";
					break;
				}
		}
		stringAndString(&pfs, &pfs_l, "\r\nContent-Type: ");
		stringAndString(&pfs, &pfs_l, ct);
		stringAndString(&pfs, &pfs_l,
				"\r\nContent-Transfer-Encoding: ");
		stringAndString(&pfs, &pfs_l, ce);
		stringAndString(&pfs, &pfs_l, "\r\n\r\n");
		stringAndString(&pfs, &pfs_l, val);
		stringAndString(&pfs, &pfs_l, eol);
		if (isfile)
			nzFree(enc);
		break;
	}			/* switch */

	return true;
}				/* postNameVal */

static bool formSubmit(const Tag *form, const Tag *submit)
{
	const Tag *t;
	int j, itype;
	char *name, *dynamicvalue = NULL;
/* dynamicvalue needs to be freed with nzFree. */
	const char *value;
	char fsep = '&';	/* field separator */
	bool rc;
	bool bval;

/* js could rebuild an option list then submit the form. */
	rebuildSelectors();

	if (form->bymail)
		fsep = '\n';
	if (form->mime) {
		fsep = '-';
		boundary = makeBoundary();
		stringAndString(&pfs, &pfs_l, "`mfd~");
		stringAndString(&pfs, &pfs_l, boundary);
		stringAndString(&pfs, &pfs_l, eol);
	}

	for (t = cw->inputlist; t; t = t->same) {
		if (t->controller != form)
			continue;
		itype = t->itype;
		if (itype <= INP_SUBMIT && t != submit)
			continue;
		name = t->name;
		if (!name)
			name = t->id;

		if (t == submit) {	/* the submit button you pushed */
			int namelen;
			char *nx;
			if (!name)
				continue;
			value = t->value;
			if (!value || !*value)
				value = "Submit";
			if (t->itype != INP_IMAGE)
				goto success;
			namelen = strlen(name);
			nx = (char *)allocMem(namelen + 3);
			strcpy(nx, name);
			strcpy(nx + namelen, ".x");
			postNameVal(nx, "0", fsep, false);
			nx[namelen + 1] = 'y';
			postNameVal(nx, "0", fsep, false);
			nzFree(nx);
			goto success;
		}

		if (itype >= INP_RADIO) {
			value = t->value;
			bval = fetchBoolVar(t);
			if (!bval)
				continue;
			if (!name)
				if (value && !*value)
					value = 0;
			if (itype == INP_CHECKBOX && value == 0)
				value = "on";
			goto success;
		}

		if (itype < INP_FILE) {
/* Even a hidden variable can be adjusted by js.
 * fetchTextVar allows for this possibility.
 * I didn't allow for it in the above, the value of a radio button;
 * hope that's not a problem. */
			dynamicvalue = fetchTextVar(t);
			postNameVal(name, dynamicvalue, fsep, false);
			nzFree(dynamicvalue);
			dynamicvalue = NULL;
			continue;
		}

		if (itype == INP_TA) {
			int cx = t->lic;
			char *cxbuf;
			int cxlen;
			if (cx) {
				if (fsep == '-') {
					char cxstring[12];
/* do this as an attachment */
					sprintf(cxstring, "%d", cx);
					if (!postNameVal
					    (name, cxstring, fsep, 1))
						goto fail;
					continue;
				}	/* attach */
				if (!unfoldBuffer(cx, true, &cxbuf, &cxlen))
					goto fail;
				for (j = 0; j < cxlen; ++j)
					if (cxbuf[j] == 0) {
						setError(MSG_SessionNull, cx);
						nzFree(cxbuf);
						goto fail;
					}
				if (j && cxbuf[j - 1] == '\n')
					--j;
				if (j && cxbuf[j - 1] == '\r')
					--j;
				cxbuf[j] = 0;
				rc = postNameVal(name, cxbuf, fsep, false);
				nzFree(cxbuf);
				if (rc)
					continue;
				goto fail;
			}

			postNameVal(name, 0, fsep, false);
			continue;
		}

		if (itype == INP_SELECT) {
			char *display = getFieldFromBuffer(t->seqno);
			char *s, *e;
			if (!display) {	/* off the air */
				Tag *v;
/* revert back to reset state */
				for (v = cw->optlist; v; v = v->same)
					if (v->controller == t)
						v->checked = v->rchecked;
				display = displayOptions(t);
			}
			rc = locateOptions(t, display, 0, &dynamicvalue, false);
			nzFree(display);
			if (!rc)
				goto fail;	/* this should never happen */
/* option could have an empty value, usually the null choice,
 * before you have made a selection. */
			if (!*dynamicvalue) {
				if (!t->multiple)
					postNameVal(name, dynamicvalue, fsep,
						    false);
				continue;
			}
/* Step through the options */
			for (s = dynamicvalue; *s; s = e) {
				char more;
				e = 0;
				if (t->multiple)
					e = strchr(s, '\1');
				if (!e)
					e = s + strlen(s);
				more = *e, *e = 0;
				postNameVal(name, s, fsep, false);
				if (more)
					++e;
			}
			nzFree(dynamicvalue);
			dynamicvalue = NULL;
			continue;
		}

		if (itype == INP_FILE) {	/* the only one left */
			dynamicvalue = fetchTextVar(t);
			if (!dynamicvalue)
				continue;
			if (!*dynamicvalue)
				continue;
			if (!(form->post & form->mime)) {
				setError(MSG_FilePost);
				nzFree(dynamicvalue);
				goto fail;
			}
			rc = postNameVal(name, dynamicvalue, fsep, 3);
			nzFree(dynamicvalue);
			dynamicvalue = NULL;
			if (rc)
				continue;
			goto fail;
		}

		i_printfExit(MSG_UnexSubmitForm);

success:
		postNameVal(name, value, fsep, false);
	}			/* loop over tags */

	if (form->mime) {	/* the last boundary */
		stringAndString(&pfs, &pfs_l, "--");
		stringAndString(&pfs, &pfs_l, boundary);
		stringAndString(&pfs, &pfs_l, "--\r\n");
	}

	i_puts(MSG_FormSubmit);
	return true;

fail:
	return false;
}				/* formSubmit */

/*********************************************************************
Push the reset or submit button.
This routine must be reentrant.
You push submit, which calls this routine, which runs the onsubmit code,
which checks the fields and calls form.submit(),
which calls this routine.  Happens all the time.
*********************************************************************/

/* jSyncup has been called before we enter this function */
bool infPush(int tagno, char **post_string)
{
	Tag *t = tagList[tagno];
	Frame *f = t->f0;
	Tag *form;
	int itype;
	int actlen;
	const char *action = 0;
	char *section;
	const char *prot;
	bool rc;

	*post_string = 0;

/* If the tag is actually a form, then infPush() was invoked
 * by form.submit().
 * Revert t back to 0, since there may be multiple submit buttons
 * on the form, and we don't know which one was pushed. */
	if (t->action == TAGACT_FORM) {
		form = t;
		t = 0;
		itype = INP_SUBMIT;
	} else {
		form = t->controller;
		itype = t->itype;
	}

	if (itype > INP_SUBMIT) {
		setError(MSG_NoButton);
		return false;
	}

	if (t) {
		if (inputDisabled(t)) {
			setError(MSG_Disabled);
			return false;
		}
		if (tagHandler(t->seqno, "onclick") && !allowJS)
			runningError(itype ==
				     INP_BUTTON ? MSG_NJNoAction :
				     MSG_NJNoOnclick);
		bubble_event(t, "onclick");
		if (js_redirects)
			return true;
// At this point onclick has run, be it button or submit or reset
	}

	if (itype == INP_BUTTON) {
/* I use to error here, but click could be captured by a node higher up in the tree
   and do what it is suppose to do, so we might not want an error here.
		if (allowJS && t->jv && !t->onclick) {
			setError(MSG_ButtonNoJS);
			return false;
		}
*/
		return true;
	}
// Now submit or reset
	if (itype == INP_RESET) {
		if (!form) {
			setError(MSG_NotInForm);
			return false;
		}
// Before we reset, run the onreset code.
// I read somewhere that onreset and onsubmit only run if you
// pushed the button - rather like onclick.
// Thus t, the reset button, must be nonzero.
		if (t && tagHandler(form->seqno, "onreset")) {
			if (!allowJS)
				runningError(MSG_NJNoReset);
			else {
				rc = true;
				if (form->jv)
					rc = run_event_bool(f, form->jv,
							    "form", "onreset");
				if (!rc)
					return true;
				if (js_redirects)
					return true;
			}
		}		/* onreset */
		formReset(form);
		return true;
	}
// now it's submit
	if (!form && !(t && t->onclick)) {
		setError(MSG_NotInForm);
		return false;
	}
// <button> could turn into submit, which we don't want to do if it is not in a form.
	if (!form)
		return true;
	// Before we submit, run the onsubmit code
	if (t && tagHandler(form->seqno, "onsubmit")) {
		if (!allowJS)
			runningError(MSG_NJNoSubmit);
		else {
			rc = true;
			if (form->jv)
				rc = bubble_event(form, "onsubmit");
			if (!rc)
				return true;
			if (js_redirects)
				return true;
		}
	}

	action = form->href;
/* But we defer to the js variable */
	if (form->jv && allowJS) {
		char *jh = get_property_url(f, form->jv, true);
		if (jh && (!action || !stringEqual(jh, action))) {
			nzFree(form->href);
			action = form->href = jh;
			jh = NULL;
		}
		nzFree(jh);
	}
// if no action, or action is "#", the default is the current location.
// And yet, with onclick on the submit button, no action means no action,
// and I believe the same is true for onsubmit.
// Just assume javascript has done the submit.
	if (!action || !*action || stringEqual(action, "#")) {
		if (t && (t->onclick | form->onsubmit))
			return true;
		action = f->hbase;
	}

	prot = getProtURL(action);
	if (!prot) {
		if (t && t->onclick)
			return true;
		setError(MSG_FormBadURL);
		return false;
	}

	debugPrint(2, "* %s", action);

	if (stringEqualCI(prot, "javascript")) {
		if (!allowJS) {
			setError(MSG_NJNoForm);
			return false;
		}
		jsRunScript(f, form->jv, action, 0, 0);
		return true;
	}

	form->bymail = false;
	if (stringEqualCI(prot, "mailto")) {
		if (!validAccount(localAccount))
			return false;
		form->bymail = true;
	} else if (stringEqualCI(prot, "http")) {
		if (form->secure) {
			setError(MSG_BecameInsecure);
			return false;
		}
	} else if (!stringEqualCI(prot, "https") &&
		   !stringEqualCI(prot, "gopher")) {
		setError(MSG_SubmitProtBad, prot);
		return false;
	}

	pfs = initString(&pfs_l);
	stringAndString(&pfs, &pfs_l, action);
	section = findHash(pfs);
	if (section) {
		i_printf(MSG_SectionIgnored, section);
		*section = 0;
		pfs_l = section - pfs;
	}
	section = strpbrk(pfs, "?\1");
	if (section && (*section == '\1' || !(form->bymail | form->post))) {
		debugPrint(3,
			   "the url already specifies some data, which will be overwritten by the data in this form");
		*section = 0;
		pfs_l = section - pfs;
	}

	stringAndChar(&pfs, &pfs_l, (form->post ? '\1' : '?'));
	actlen = strlen(pfs);

	if (!formSubmit(form, t)) {
		nzFree(pfs);
		return false;
	}

	debugPrint(3, "%s %s", form->post ? "post" : "get", pfs + actlen);

/* Handle the mail method here and now. */
	if (form->bymail) {
		char *addr, *subj, *q;
		const char *tolist[2], *atlist[2];
		const char *name = form->name;
		int newlen = strlen(pfs) - actlen;	/* the new string could be longer than post */
		decodeMailURL(action, &addr, &subj, 0);
		tolist[0] = addr;
		tolist[1] = 0;
		atlist[0] = 0;
		newlen += 9;	/* subject: \n */
		if (subj)
			newlen += strlen(subj);
		else
			newlen += 11 + (name ? strlen(name) : 1);
		++newlen;	/* null */
		++newlen;	/* encodeAttachment might append another nl */
		q = (char *)allocMem(newlen);
		if (subj)
			sprintf(q, "subject:%s\n", subj);
		else
			sprintf(q, "subject:html form(%s)\n",
				name ? name : "?");
		strcpy(q + strlen(q), pfs + actlen);
		nzFree(pfs);
		i_printf(MSG_MailSending, addr);
		SLEEP(1);
		rc = sendMail(localAccount, tolist, q, -1, atlist, 0, 0, false);
		if (rc)
			i_puts(MSG_MailSent);
		nzFree(addr);
		nzFree(subj);
		nzFree(q);
		*post_string = 0;
		return rc;
	}
// gopher submit is one input field with no name;
// the leading = doesn't belong.
	if (pfs[actlen] == '=' && stringEqualCI(prot, "gopher"))
		strmove(pfs + actlen, pfs + actlen + 1);

	*post_string = pfs;
	return true;
}				/* infPush */

/* I don't have any reverse pointers, so I'm just going to scan the list */
/* This doesn't come up all that often. */
Tag *tagFromJavaVar(jsobjtype v)
{
	Tag *t = 0;
	int i;

	if (!tagList)
		i_printfExit(MSG_NullListInform);

	for (i = 0; i < cw->numTags; ++i) {
		t = tagList[i];
		if (t->jv == v && !t->dead)
			return t;
	}
	return 0;
}				/* tagFromJavaVar */

// Create a new tag for this pointer, only from document.createElement().
static Tag *tagFromJavaVar2(jsobjtype v, const char *tagname)
{
	Tag *t;
	if (!tagname)
		return 0;
	t = newTag(cf, tagname);
	if (!t) {
		debugPrint(3, "cannot create tag node %s", tagname);
		return 0;
	}
	connectTagObject(t, v);
/* this node now has a js object, don't decorate it again. */
	t->step = 2;
/* and don't render it unless it is linked into the active tree */
	t->deleted = true;
	return t;
}				/* tagFromJavaVar2 */

/* Return false to stop javascript, due to a url redirect */
void javaSubmitsForm(jsobjtype v, bool reset)
{
	if (reset)
		js_reset = v;
	else
		js_submit = v;
}				/* javaSubmitsForm */

bool bubble_event(const Tag *t, const char *name)
{
	Frame *f = t->f0;
	jsobjtype e;		// the event object
	bool rc;
	if (!allowJS || !t->jv)
		return true;
	e = create_event(f, t->jv, name);
	rc = run_function_onearg(f, t->jv, "dispatchEvent", e);
	if (rc && get_property_bool(f, e, "prev$default"))
		rc = false;
	unlink_event(f, t->jv);
	return rc;
}				/* bubble_event */

/* Javascript errors, we need to see these no matter what. */
void runningError(int msg, ...)
{
	va_list p;
	if (ismc)
		return;
	if (debugLevel <= 2)
		return;
	va_start(p, msg);
	vprintf(i_getString(msg), p);
	va_end(p);
	nl();
}				/* runningError */

/*********************************************************************
Diff the old screen with the new rendered screen.
This is a simple front back diff algorithm.
Compare the two strings from the start, how many lines are the same.
Compare the two strings from the back, how many lines are the same.
That zeros in on the line that has changed.
Most of the time one line has changed,
or a couple of adjacent lines, or a couple of nearby lines.
So this should do it.
sameFront counts the lines from the top that are the same.
We're here because the buffers are different, so sameFront will not equal $.
Lines after sameFront are different.
Lines past sameBack1 and same back2 are the same to the bottom in the two buffers.
To be a bit more sophisticated, front1z and front2z
become nonzero if just one line was added, updated, or deleted at sameFront.
they march on beyond this point, as long as lines are the same.
In the same way, back1z and back2z march backwards
past a one line anomaly.
*********************************************************************/

static int sameFront, sameBack1, sameBack2;
static int front1z, front2z, back1z, back2z;
static const char *newChunkStart, *newChunkEnd;

// need a reverse strchr to help us out.
static const char *rstrchr(const char *s, const char *mark)
{
	for (--s; s > mark; --s)
		if (s[-1] == '\n')
			return s;
	return (s == mark ? s : NULL);
}

static void frontBackDiff(const char *b1, const char *b2)
{
	const char *f1, *f2, *s1, *s2, *e1, *e2;
	const char *g1, *g2, *h1, *h2;

	sameFront = front1z = front2z = 0;
	back1z = back2z = 0;

	s1 = b1, s2 = b2;
	f1 = b1, f2 = b2;
	while (*s1 == *s2 && *s1) {
		if (*s1 == '\n') {
			f1 = s1 + 1, f2 = s2 + 1;
			++sameFront;
		}
		++s1, ++s2;
	}

	g1 = strchr(f1, '\n');
	g2 = strchr(f2, '\n');
	if (g1 && g2) {
		++g1, ++g2;
		h1 = strchr(g1, '\n');
		h2 = strchr(g2, '\n');
		if (h1 && h2) {
			++h1, ++h2;
			if (g1 - f1 == h2 - g2 && !memcmp(f1, g2, g1 - f1)) {
				e1 = f1, e2 = g2;
				s1 = g1, s2 = h2;
				front1z = sameFront + 1;
				front2z = sameFront + 2;
			} else if (h1 - g1 == g2 - f2
				   && !memcmp(g1, f2, h1 - g1)) {
				e1 = g1, e2 = f2;
				s1 = h1, s2 = g2;
				front1z = sameFront + 2;
				front2z = sameFront + 1;
			} else if (h1 - g1 == h2 - g2
				   && !memcmp(g1, g2, h1 - g1)) {
				e1 = g1, e2 = g2;
				s1 = h1, s2 = h2;
				front1z = sameFront + 2;
				front2z = sameFront + 2;
			}
		}
	}

	if (front1z || front2z) {
		sameBack1 = front1z - 1, sameBack2 = front2z - 1;
		while (*s1 == *s2 && *s1) {
			if (*s1 == '\n')
				++front1z, ++front2z;
			++s1, ++s2;
		}
		if (!*s1) {
			front1z = front2z = 0;
			goto done;
		}
	}

	s1 = b1 + strlen(b1);
	s2 = b2 + strlen(b2);
	while (s1 > f1 && s2 > f2 && s1[-1] == s2[-1])
		--s1, --s2;
	if (s1 == f1 && s2[-1] == '\n')
		goto mark_e;
	if (s2 == f2 && s1[-1] == '\n')
		goto mark_e;
/* advance both pointers to newline or null */
	while (*s1 && *s1 != '\n')
		++s1, ++s2;
/* these buffers should always end in newline, so the next if should always be true */
	if (*s1 == '\n')
		++s1, ++s2;
mark_e:
	e1 = s1, e2 = s2;

	sameBack1 = sameFront;
	for (s1 = f1; s1 < e1; ++s1)
		if (*s1 == '\n')
			++sameBack1;
	if (s1 > f1 && s1[-1] != '\n')	// should never happen
		++sameBack1;

	sameBack2 = sameFront;
	for (s2 = f2; s2 < e2; ++s2)
		if (*s2 == '\n')
			++sameBack2;
	if (s2 > f2 && s2[-1] != '\n')	// should never happen
		++sameBack2;

	if (front1z || front2z) {
// front2z can run past sameBack2 if lines are deleted.
// This because front2z is computed before sameBack2.
		while (front1z > sameBack1 || front2z > sameBack2)
			--front1z, --front2z;
		if (front1z <= sameFront || front2z <= sameFront)
			front1z = front2z = 0;
		goto done;
	}

	h1 = rstrchr(e1, f1);
	h2 = rstrchr(e2, f2);
	if (h1 && h2) {
		g1 = rstrchr(h1, f1);
		g2 = rstrchr(h2, f2);
		if (g1 && g2) {
			if (e1 - h1 == h2 - g2 && !memcmp(h1, g2, e1 - h1)) {
				s1 = h1, s2 = g2;
				back1z = sameBack1, back2z = sameBack2 - 1;
			} else if (h1 - g1 == e2 - h2
				   && !memcmp(g1, h2, h1 - g1)) {
				s1 = g1, s2 = h2;
				back1z = sameBack1 - 1, back2z = sameBack2;
			} else if (h1 - g1 == h2 - g2
				   && !memcmp(g1, g2, h1 - g1)) {
				s1 = g1, s2 = g2;
				back1z = sameBack1 - 1, back2z = sameBack2 - 1;
			}
		}
	}

	if (back1z || back2z) {
		--s1, --s2;
		while (*s1 == *s2 && s1 >= f1 && s2 >= f2) {
			if (s1[-1] == '\n' && s2[-1] == '\n')
				--back1z, --back2z;
			--s1, --s2;
		}
	}

done:
	newChunkStart = f2;
	newChunkEnd = e2;
}				/* frontBackDiff */

// Believe it or not, I have exercised all the pathways in this routine.
// It's rather mind numbing.
static bool reportZ(void)
{
// low and high operations are ad, update, delete
	char oplow, ophigh;
// lines affected in the second group
	int act1, act2;
	int d_start, d_end;

	if (!(front1z || front2z || back1z || back2z))
		return false;

	if (front1z || front2z) {
		if (front2z > front1z)
			oplow = 1;
		if (front2z == front1z)
			oplow = 2;
		if (front2z < front1z)
			oplow = 3;
		act1 = sameBack1 - front1z;
		act2 = sameBack2 - front2z;
		ophigh = 2;
		if (!act1)
			ophigh = 1;
		if (!act2)
			ophigh = 3;
// delete delete is the easy case, but very rare
		if (oplow == 3 && ophigh == 3) {
			if (act1 == 1)
				i_printf(MSG_LineDeleteZ1, sameFront + 1,
					 sameBack1);
			else
				i_printf(MSG_LineDeleteZ2, sameFront + 1,
					 front1z + 1, sameBack1);
			goto done;
		}
// double add is more common, and also unambiguous.
// If this algorithm says we added 100 lines, then we added 100 lines.
		if (oplow == 1 && ophigh == 1) {
			if (act2 == 1)
				i_printf(MSG_LineAddZ1, sameFront + 1,
					 sameBack2);
			else
				i_printf(MSG_LineAddZ2, sameFront + 1,
					 front2z + 1, sameBack2);
			goto done;
		}
		if (oplow == 3) {
// delete mixed with something else, and I just don't care about the delete.
			if (ophigh == 1)
				i_printf(MSG_LineAdd2, front2z + 1, sameBack2);
			else if (act2 <= 10)
				i_printf(MSG_LineUpdate3, front2z + 1,
					 sameBack2);
			else
				i_printf(MSG_LineUpdateRange, front2z + 1,
					 sameBack2);
			goto done;
		}
		if (ophigh == 3) {
// if the deleted block is big then report it, otherwise ignore it.
			if (act1 >= 10)
				i_printf(MSG_LineDelete2, act1, front1z);
			else if (oplow == 1)
				i_printf(MSG_LineAdd1, sameFront + 1);
			else
				i_printf(MSG_LineUpdate1, sameFront + 1);
			goto done;
		}
// a mix of add and update, call it an update.
// If the second group is big then switch to range message.
		if (act2 > 10 && ophigh == 2)
			i_printf(MSG_LineUpdateRange,
				 (front2z - sameFront <
				  10 ? sameFront + 1 : front2z + 1), sameBack2);
		else if (act2 == 1)
			i_printf(MSG_LineUpdateZ1, sameFront + 1, sameBack2);
		else
			i_printf(MSG_LineUpdateZ2, sameFront + 1, front2z + 1,
				 sameBack2);
		goto done;
	}
// At this point the single line change comes second,
// we have to look at back1z and back2z.
	d_start = sameBack2 - sameBack1;
	d_end = back2z - back1z;
	ophigh = 2;
	if (d_end > d_start)
		ophigh = 3;
	if (d_end < d_start)
		ophigh = 1;
	act1 = back1z - sameFront - 1;
	act2 = back2z - sameFront - 1;
	oplow = 2;
	if (!act1)
		oplow = 1;
	if (!act2)
		oplow = 3;
// delete delete is the easy case, but very rare
	if (oplow == 3 && ophigh == 3) {
// act1 should never be 1, because then one line was deleted earlier,
// and we would be in the front1z case.
		i_printf(MSG_LineDeleteZ3, sameFront + 1, back1z - 1,
			 sameBack1);
		goto done;
	}
// double add is more common, and also unambiguous.
// If this algorithm says we added 100 lines, then we added 100 lines.
	if (oplow == 1 && ophigh == 1) {
		i_printf(MSG_LineAddZ3, sameFront + 1, back2z - 1, sameBack2);
		goto done;
	}
	if (ophigh == 3) {
// delete mixed with something else, and I just don't care about the delete.
		if (oplow == 1)
			i_printf(MSG_LineAdd2, sameFront + 1, back2z - 1);
		else if (act2 <= 10)
			i_printf(MSG_LineUpdate3, sameFront + 1, back2z - 1);
		else
			i_printf(MSG_LineUpdateRange, sameFront + 1,
				 back2z - 1);
		goto done;
	}
	if (oplow == 3) {
// if the deleted block is big then report it, otherwise ignore it.
		if (act1 >= 10)
			i_printf(MSG_LineDelete2, act1, sameFront);
		else if (ophigh == 1)
			i_printf(MSG_LineAdd1, sameBack2);
		else
			i_printf(MSG_LineUpdate1, sameBack2);
		goto done;
	}
// a mix of add and update, call it an update.
// If the first group is big then switch to range message.
	if (act2 > 10 && oplow == 2)
		i_printf(MSG_LineUpdateRange,
			 sameFront + 1,
			 (sameBack2 - back2z < 10 ? sameBack2 : back2z - 1));
	else
		i_printf(MSG_LineUpdateZ3, sameFront + 1, back2z - 1,
			 sameBack2);

done:
	return true;
}

static time_t now_sec;
static int now_ms;
static void currentTime(void)
{
	struct timeval tv;
	gettimeofday(&tv, NULL);
	now_sec = tv.tv_sec;
	now_ms = tv.tv_usec / 1000;
}				/* currentTime */

static void silent(int msg, ...)
{
}

// Is there an active tag below?
static bool activeBelow(Tag *t)
{
	bool rc;
	int action = t->action;
	if (action == TAGACT_INPUT || action == TAGACT_SELECT ||
	    action == TAGACT_A || (action == TAGACT_SPAN && t->onclick))
		return true;
	t = t->firstchild;
	while (t) {
		rc = activeBelow(t);
		if (rc)
			return rc;
		t = t->sibling;
	}
	return false;
}

static int hovcount, invcount, injcount;

/* Rerender the buffer and notify of any lines that have changed */
int rr_interval = 20;
void rerender(bool rr_command)
{
	char *a, *snap, *newbuf;
	int j;
	int markdot, wasdot, addtop;
	bool z;
	void (*say_fn) (int, ...);

	debugPrint(4, "rerender");
	cw->mustrender = false;
	time(&cw->nextrender);
	cw->nextrender += rr_interval;
	hovcount = invcount = injcount = 0;

// not sure if we have to do this here
	rebuildSelectors();

	if (rr_command) {
// You might have changed some input fields on the screen, then typed rr
		jSyncup(true);
	}
// screen snap, to compare with the new screen.
	if (!unfoldBufferW(cw, false, &snap, &j)) {
		snap = 0;
		puts("no screen snap available");
		return;
	}

/* and the new screen */
	a = render(0);
	newbuf = htmlReformat(a);
	nzFree(a);

	if (rr_command && debugLevel >= 3) {
		char buf[120];
		buf[0] = 0;
		if (hovcount)
			sprintf(buf, "%d nodes under hover", hovcount);
		if (invcount) {
			if (buf[0])
				strcat(buf, ", ");
			sprintf(buf + strlen(buf),
				"%d nodes invisible", invcount);
		}
		if (injcount) {
			if (buf[0])
				strcat(buf, ", ");
			sprintf(buf + strlen(buf), "%d nodes injected by css",
				injcount);
		}
		if (buf[0])
			debugPrint(3, "%s", buf);
	}

/* the high runner case, most of the time nothing changes,
 * and we can check that efficiently with strcmp */
	if (stringEqual(newbuf, snap)) {
		if (rr_command)
			i_puts(MSG_NoChange);
		nzFree(newbuf);
		nzFree(snap);
		return;
	}

/* mark dot, so it stays in place */
	cw->labels[MARKDOT] = wasdot = cw->dot;
	frontBackDiff(snap, newbuf);
	addtop = 0;
	if (sameBack1 > sameFront)
		delText(sameFront + 1, sameBack1);
	if (sameBack2 > sameFront) {
		addTextToBuffer((pst) newChunkStart,
				newChunkEnd - newChunkStart, sameFront, false);
		addtop = sameFront + 1;
	}
	markdot = cw->labels[MARKDOT];
	if (markdot)
		cw->dot = markdot;
	else if (sameBack1 == sameBack2)
		cw->dot = wasdot;
	else if (addtop)
		cw->dot = addtop;
	cw->undoable = false;

/*********************************************************************
It's almost easier to do it than to report it.
First, run diff again with the hidden numbers gone, so we only report
the visible differences. It's annoying to hear that line 27 has been updated,
and it looks just like it did before.
This happens when a periodic timer updates a section through innerHTML.
If the text is the same every time that's fine, but it's new tags each time,
and new internal numbers each time, and that use to trip this algorithm.
*********************************************************************/

	removeHiddenNumbers((pst) snap, 0);
	removeHiddenNumbers((pst) newbuf, 0);
	if (stringEqual(snap, newbuf)) {
		if (rr_command)
			i_puts(MSG_NoChange);
		goto done;
	}
	frontBackDiff(snap, newbuf);
	debugPrint(4, "front %d back %d,%d front z %d,%d back z %d,%d",
		   sameFront, sameBack1, sameBack2,
		   front1z, front2z, back1z, back2z);
	z = reportZ();

// Even if the change has been reported above,
// I march on here because it puts dot back where it belongs.
	say_fn = (z ? silent : i_printf);
	if (sameBack2 == sameFront) {	/* delete */
		if (sameBack1 == sameFront + 1)
			(*say_fn) (MSG_LineDelete1, sameFront);
		else
			(*say_fn) (MSG_LineDelete2, sameBack1 - sameFront,
				   sameFront);
	} else if (sameBack1 == sameFront) {
		if (sameBack2 == sameFront + 1)
			(*say_fn) (MSG_LineAdd1, sameFront + 1);
		else {
			(*say_fn) (MSG_LineAdd2, sameFront + 1, sameBack2);
/* put dot back to the start of the new block */
			if (!markdot)
				cw->dot = sameFront + 1;
		}
	} else {
		if (sameBack1 == sameFront + 1 && sameBack2 == sameFront + 1)
			(*say_fn) (MSG_LineUpdate1, sameFront + 1);
		else if (sameBack2 == sameFront + 1)
			(*say_fn) (MSG_LineUpdate2, sameBack1 - sameFront,
				   sameFront + 1);
		else {
			if (sameBack2 - sameFront <= 10 ||
			    sameBack1 - sameFront <= 10)
				(*say_fn) (MSG_LineUpdate3, sameFront + 1,
					   sameBack2);
			else
				(*say_fn) (MSG_LineUpdateRange, sameFront + 1,
					   sameBack2);
/* put dot back to the start of the new block */
			if (!markdot && sameBack1 != sameBack2)
				cw->dot = sameFront + 1;
		}
	}

done:
	nzFree(newbuf);
	nzFree(snap);
}				/* rerender */

/* mark the tags on the deleted lines as deleted */
void delTags(int startRange, int endRange)
{
	pst p;
	int j, tagno, action;
	Tag *t;

/* no javascript, no cause to ever rerender */
	if (!cf->cx)
		return;

	for (j = startRange; j <= endRange; ++j) {
		p = fetchLine(j, -1);
		for (; *p != '\n'; ++p) {
			if (*p != InternalCodeChar)
				continue;
			tagno = strtol((char *)p + 1, (char **)&p, 10);
/* could be 0, but should never be negative */
			if (tagno <= 0)
				continue;
			t = tagList[tagno];
/* Only mark certain tags as deleted.
 * If you mark <div> deleted, it could wipe out half the page. */
			action = t->action;
			if (action == TAGACT_TEXT ||
			    action == TAGACT_HR ||
			    action == TAGACT_LI || action == TAGACT_IMAGE)
				t->deleted = true;
#if 0
/* this seems to cause more trouble than it's worth */
			Tag *last_td = 0;
			if (action == TAGACT_TD) {
				printf("td%d\n", tagno);
				if (last_td)
					last_td->deleted = true;
				last_td = t;
			}
#endif
		}
	}
}				/* delTags */

/* turn an onunload function into a clickable hyperlink */
static void unloadHyperlink(const char *js_function, const char *where)
{
	dwStart();
	stringAndString(&cf->dw, &cf->dw_l, "<P>Onclose <A href='javascript:");
	stringAndString(&cf->dw, &cf->dw_l, js_function);
	stringAndString(&cf->dw, &cf->dw_l, "()'>");
	stringAndString(&cf->dw, &cf->dw_l, where);
	stringAndString(&cf->dw, &cf->dw_l, "</A><br>");
}				/* unloadHyperlink */

/* Run the various onload functions */
/* Turn the onunload functions into hyperlinks */
/* This runs after the page is parsed and before the various javascripts run, is that right? */
void runOnload(void)
{
	int i, action;
	int fn;			/* form number */
	Tag *t;

	if (!isJSAlive)
		return;
	if (intFlag)
		return;

/* window and document onload */
	run_event_bool(cf, cf->winobj, "window", "onload");
	if (intFlag)
		return;
	run_event_bool(cf, cf->docobj, "document", "onload");
	if (intFlag)
		return;

	fn = -1;
	for (i = 0; i < cw->numTags; ++i) {
		if (intFlag)
			return;
		t = tagList[i];
		if (t->slash)
			continue;
		if (t->f0 != cf)
			continue;
		action = t->action;
		if (action == TAGACT_FORM)
			++fn;
		if (!t->jv)
			continue;
		if (action == TAGACT_BODY && handlerPresent(cf, t->jv, "onload"))
			run_event_bool(cf, t->jv, "body", "onload");
		if (action == TAGACT_BODY && t->onunload)
			unloadHyperlink("document.body.onunload", "Body");
		if (action == TAGACT_FORM && handlerPresent(cf, t->jv, "onload"))
			run_event_bool(cf, t->jv, "form", "onload");
/* tidy5 says there is no form.onunload */
		if (action == TAGACT_FORM && t->onunload) {
			char formfunction[48];
			sprintf(formfunction, "document.forms[%d].onunload",
				fn);
			unloadHyperlink(formfunction, "Form");
		}
		if (action == TAGACT_H && handlerPresent(cf, t->jv, "onload"))
			run_event_bool(cf, t->jv, "h1", "onload");
	}
}				/* runOnload */

/*********************************************************************
Manage js timers here.
It's a simple list of timers, assuming there aren't too many.
Store the seconds and milliseconds when the timer should fire,
the code to execute, and the timer object, which becomes "this".
*********************************************************************/

struct jsTimer {
	struct jsTimer *next, *prev;
	Frame *frame;	/* edbrowse frame holding this timer */
	Tag *t;	// for an asynchronous script
	time_t sec;
	int ms;
	bool isInterval;
	bool running;
	bool deleted;
	int jump_sec;		/* for interval */
	int jump_ms;
	jsobjtype timerObject;
};

/* list of pending timers */
struct listHead timerList = {
	&timerList, &timerList
};

/*********************************************************************
the spec says you can't run a timer less than 10 ms but here we currently use
900 ms. This really should be a configurable limit.
If less than 200ms the load average jumps way up.  e.g.nasa.gov
We only rerender the screen every 20 seconds or so anyways.
But, the acid test uses a timer to schedule each of its 100 tests,
and is crazy slow if we throttle them.
So ... the first few timers can run as fast  as they like,and we're ok
with that, then timers slow down as we proceed.
*********************************************************************/
int timerResolution = 900;
static int tsn;			// timer sequence number

void javaSetsTimeout(int n, const char *jsrc, jsobjtype to, bool isInterval)
{
	struct jsTimer *jt;
	int seqno;

	if (jsrc[0] == 0)
		return;		/* nothing to run */

	if (stringEqual(jsrc, "-")) {
		seqno = get_property_number(cf, to, "tsn");
// delete a timer
		foreach(jt, timerList) {
			if (jt->timerObject == to) {
				debugPrint(4, "timer %d delete", seqno);
// a running timer will often delete itself.
				if (jt->running) {
					jt->deleted = true;
				} else {
					char *gc_name =
					    get_property_string(cf, jt->timerObject,
								"backlink");
					if (gc_name)
						delete_property(cf, cf->winobj,
								gc_name);
					delFromList(jt);
					nzFree(jt);
				}
				return;
			}
		}
// not found, just return.
		return;
	}

	jt = allocZeroMem(sizeof(struct jsTimer));
	if (n < timerResolution) {
		n = cf->jtmin;
		if (!n)
			n = 10;
		if (n < timerResolution)
			n += 3;
		cf->jtmin = n;
	}
	jt->sec = n / 1000;
	jt->ms = n % 1000;
	if ((jt->isInterval = isInterval))
		jt->jump_sec = n / 1000, jt->jump_ms = n % 1000;
	currentTime();
	jt->sec += now_sec;
	jt->ms += now_ms;
	if (jt->ms >= 1000)
		jt->ms -= 1000, ++jt->sec;
	jt->timerObject = to;
	jt->frame = cf;
	addToListBack(&timerList, jt);
	seqno = ++tsn;
	set_property_number(cf, to, "tsn", seqno);
	debugPrint(4, "timer %d %s", seqno, jsrc);
}				/* javaSetsTimeout */

void scriptSetsTimeout(Tag *t)
{
	struct jsTimer *jt = allocZeroMem(sizeof(struct jsTimer));
	jt->sec = 0;
	jt->ms = asyncTimer;
	jt->isInterval = true;
	jt->jump_sec = 0, jt->jump_ms = asyncTimer;
	currentTime();
	jt->sec += now_sec;
	jt->ms += now_ms;
	if (jt->ms >= 1000)
		jt->ms -= 1000, ++jt->sec;
	jt->t = t;
	jt->frame = cf;
	addToListBack(&timerList, jt);
	debugPrint(3, "timer %s%d=%s",
		   (t->action == TAGACT_SCRIPT ? "script" : "xhr"),
		   ++tsn, t->href);
	t->lic = tsn;
}				/* scriptSetsTimeout */

static struct jsTimer *soonest(void)
{
	struct jsTimer *t, *best_t = 0;
	if (listIsEmpty(&timerList))
		return 0;
	foreach(t, timerList) {
		if (!best_t || t->sec < best_t->sec ||
		    (t->sec == best_t->sec && t->ms < best_t->ms))
			best_t = t;
	}
	return best_t;
}				/* soonest */

bool timerWait(int *delay_sec, int *delay_ms)
{
	struct jsTimer *jt = soonest();
	time_t now;
	int remaining;

	if (cw->mustrender) {
		time(&now);
		remaining = 0;
		if (now < cw->nextrender)
			remaining = cw->nextrender - now;
	}

	if (!jt) {
		if (!cw->mustrender)
			return false;
		*delay_sec = remaining;
		*delay_ms = 0;
		return true;
	}

	currentTime();
	if (now_sec > jt->sec || (now_sec == jt->sec && now_ms >= jt->ms))
		*delay_sec = *delay_ms = 0;
	else {
		*delay_sec = jt->sec - now_sec;
		*delay_ms = (jt->ms - now_ms);
		if (*delay_ms < 0)
			*delay_ms += 1000, --*delay_sec;
	}

	if (cw->mustrender && remaining <= *delay_sec) {
		*delay_sec = remaining;
		*delay_ms = 0;
	}

	return true;
}				/* timerWait */

void delTimers(Frame *f)
{
	int delcount = 0;
	struct jsTimer *jt, *jnext;
	for (jt = timerList.next; jt != (void *)&timerList; jt = jnext) {
		jnext = jt->next;
		if (jt->frame == f) {
			++delcount;
			delFromList(jt);
			nzFree(jt);
		}
	}
	debugPrint(4, "%d timers deleted", delcount);
}				/* delTimers */

void runTimer(void)
{
	struct jsTimer *jt;
	struct ebWindow *save_cw = cw;
	Frame *save_cf = cf;
	Tag *t;

	currentTime();

	if ((jt = soonest())
	    && !(jt->sec > now_sec || (jt->sec == now_sec && jt->ms > now_ms))) {

		if (!gotimers)
			goto skip_execution;

		cf = jt->frame;
		cw = cf->owner;

/*********************************************************************
Only syncing the foreground window is right almost all the time,
but not every time.
The forground could be just text, buffer for a textarea in another window.
You should sync that other window before running javascript, so it has
the latest text, the text you are editing right now.
I can't do that because jSyncup calls fetchLine() to pull text lines
out of the buffer, which has to be the foreground window.
We need to fix this someday, though it is a very rare low runner case.
*********************************************************************/
		if (foregroundWindow)
			jSyncup(true);
		jt->running = true;
		if ((t = jt->t)) {
// asynchronous script
			if (t->step == 3) {	// background load
				int rc =
				    pthread_tryjoin_np(t->loadthread, NULL);
				if (rc != 0 && rc != EBUSY) {
// should never happen
					debugPrint(3,
						   "script background thread test returns %d",
						   rc);
					pthread_join(t->loadthread, NULL);
					rc = 0;
				}
				if (!rc) {	// it's done
					if (!t->loadsuccess) {
						if (debugLevel >= 3)
							i_printf(MSG_GetJS,
								 t->href,
								 t->hcode);
						t->step = 6;
					} else {
						if (t->action == TAGACT_SCRIPT) {
							set_property_string(cf, 
							    t->jv, "text",
							     t->value);
							nzFree(t->value);
							t->value = 0;
						}
						t->step = 4;	// loaded
					}
				}
			}
			if (t->step == 4 && t->action == TAGACT_SCRIPT) {
				char *js_file = t->js_file;
				int ln = t->js_ln;
				t->step = 5;	// running
				if (!js_file)
					js_file = "generated";
				if (!ln)
					ln = 1;
				if (ln > 1)
					++ln;
				if (cf != save_cf)
					debugPrint(4,
						   "running script at a lower frame %s",
						   js_file);
				debugPrint(3, "async exec timer %d %s at %d",
					   t->lic, js_file, ln);
				set_property_object(cf, cf->docobj, "currentScript",
						    t->jv);
				jsRunData(cf, t->jv, js_file, ln);
				if (t->js_file && !isDataURI(t->href) && handlerPresent(cf, t->jv, "onload"))
					run_event_bool(cf, t->jv, "script", "onload");
				delete_property(cf, cf->docobj, "currentScript");
				debugPrint(3, "async exec complete");
			}
			if (t->step == 4 && t->action != TAGACT_SCRIPT) {
				t->step = 5;
				set_property_string(cf, t->jv, "$entire", t->value);
// could be large; it's worth freeing
				nzFree(t->value);
				t->value = 0;
				debugPrint(3, "run xhr %d", t->lic);
				run_function_bool(cf, t->jv, "parseResponse");
				jt->timerObject = t->jv;
			}
			if (t->step >= 5)
				jt->deleted = true;
		} else {
			run_function_bool(cf, jt->timerObject, "ontimer");
		}
		jt->running = false;
skip_execution:

		if (!jt->isInterval || jt->deleted) {
			if (jt->timerObject) {
				char *gc_name =
				    get_property_string(cf, jt->timerObject,
							"backlink");
				if (gc_name)
					delete_property(cf, cf->winobj, gc_name);
			}
			delFromList(jt);
			nzFree(jt);
		} else {
			jt->sec = now_sec + jt->jump_sec;
			jt->ms = now_ms + jt->jump_ms;
			if (jt->ms >= 1000)
				jt->ms -= 1000, ++jt->sec;
		}

		if (gotimers)
			jSideEffects();
	}

	cw = save_cw;
	cf = save_cf;
}				/* runTimer */

void javaOpensWindow(const char *href, const char *name)
{
	char *copy, *r;
	const char *a;
	bool replace = false;

	if (*href == 'r')
		replace = true;
	++href;
	if (!*href) {
		debugPrint(3, "javascript is opening a blank window");
		return;
	}

	copy = cloneString(href);
	unpercentURL(copy);
	r = resolveURL(cf->hbase, copy);
	nzFree(copy);
	if ((replace || cw->browseMode) && foregroundWindow) {
		gotoLocation(r, 0, replace);
		return;
	}

/* Turn the new window into a hyperlink. */
/* just shovel this onto dw, as though it came from document.write() */
	dwStart();
	stringAndString(&cf->dw, &cf->dw_l, "<P>");
	stringAndString(&cf->dw, &cf->dw_l,
			i_getString(replace ? MSG_Redirect : MSG_NewWindow));
	stringAndString(&cf->dw, &cf->dw_l, ": <A href=");
	stringAndString(&cf->dw, &cf->dw_l, r);
	stringAndChar(&cf->dw, &cf->dw_l, '>');
	a = altText(r);
	nzFree(r);
/* I'll assume this is more helpful than the name of the window */
	if (a)
		name = a;
	r = htmlEscape(name);
	stringAndString(&cf->dw, &cf->dw_l, r);
	nzFree(r);
	stringAndString(&cf->dw, &cf->dw_l, "</A><br>\n");
}				/* javaOpensWindow */

/* Push an attribute onto an html tag. */
/* Value is already allocated, name is not. */
/* So far only used by javaSetsLinkage. */
static void setTagAttr(Tag *t, const char *name, char *val)
{
	int nattr = 0;		/* number of attributes */
	int i = -1;
	if (!val)
		return;
	if (t->attributes) {
		for (nattr = 0; t->attributes[nattr]; ++nattr)
			if (stringEqualCI(name, t->attributes[nattr]))
				i = nattr;
	}
	if (i >= 0) {
		cnzFree(t->atvals[i]);
		t->atvals[i] = val;
		return;
	}
/* push */
	if (!nattr) {
		t->attributes = allocMem(sizeof(char *) * 2);
		t->atvals = allocMem(sizeof(char *) * 2);
	} else {
		t->attributes =
		    reallocMem(t->attributes, sizeof(char *) * (nattr + 2));
		t->atvals = reallocMem(t->atvals, sizeof(char *) * (nattr + 2));
	}
	t->attributes[nattr] = cloneString(name);
	t->atvals[nattr] = val;
	++nattr;
	t->attributes[nattr] = 0;
	t->atvals[nattr] = 0;
}				/* setTagAttr */

void javaSetsLinkage(bool after, char type, jsobjtype p_j, const char *rest)
{
	Tag *parent, *add, *before, *c, *t;
	jsobjtype *a_j, *b_j;
	jsobjtype cx;
	char p_name[MAXTAGNAME], a_name[MAXTAGNAME], b_name[MAXTAGNAME];
	int action;
	char *jst;		// javascript string

// Some functions in third.js create, link, and then remove nodes, before
// there is a document. Don't run any side effects in this case.
	if (!cw->tags)
		return;

	sscanf(rest, "%s %p,%s %p,%s ", p_name, &a_j, a_name, &b_j, b_name);
	if (type == 'c') {	/* create */
		parent = tagFromJavaVar2(p_j, p_name);
		if (parent) {
			debugPrint(4, "linkage, %s %d created",
				   p_name, parent->seqno);
			if (parent->action == TAGACT_INPUT) {
// we need to establish the getter and setter for value
				set_property_string_0(parent->f0->cx,
				parent->jv, "value", emptyString);
			}
		}
		return;
	}

	parent = tagFromJavaVar(p_j);
/* options are relinked by rebuildSelectors, not here. */
	if (stringEqual(p_name, "option"))
		return;

	if (stringEqual(a_name, "option"))
		return;

	add = tagFromJavaVar(a_j);
	if (!parent || !add)
		return;

	if (type == 'r') {
/* add is a misnomer here, it's being removed */
		add->deleted = true;
		debugPrint(4, "linkage, %s %d removed from %s %d",
			   a_name, add->seqno, p_name, parent->seqno);
		add->parent = NULL;
		if (parent->firstchild == add)
			parent->firstchild = add->sibling;
		else {
			c = parent->firstchild;
			if (c) {
				for (; c->sibling; c = c->sibling) {
					if (c->sibling != add)
						continue;
					c->sibling = add->sibling;
					break;
				}
			}
		}
		add->sibling = NULL;
		return;
	}

/* check and see if this link would turn the tree into a circle, whence
 * any subsequent traversal would fall into an infinite loop.
 * Child node must not have a parent, and, must not link into itself.
 * Oddly enough the latter seems to happen on acid3.acidtests.org,
 * linking body into body, and body at the top has no parent,
 * so passes the "no parent" test, whereupon I had to add the second test. */
	if (add->parent || add == parent) {
		if (debugLevel >= 3) {
			debugPrint(3,
				   "linkage cycle, cannot link %s %d into %s %d",
				   a_name, add->seqno, p_name, parent->seqno);
			if (type == 'b') {
				before = tagFromJavaVar(b_j);
				debugPrint(3, "before %s %d", b_name,
					   (before ? before->seqno : -1));
			}
			if (add->parent)
				debugPrint(3,
					   "the child already has parent %s %d",
					   add->parent->info->name,
					   add->parent->seqno);
			debugPrint(3,
				   "Aborting the link, some data may not be rendered.");
		}
		return;
	}

	if (type == 'b') {	/* insertBefore */
		before = tagFromJavaVar(b_j);
		if (!before)
			return;
		debugPrint(4, "linkage, %s %d linked into %s %d before %s %d",
			   a_name, add->seqno, p_name, parent->seqno,
			   b_name, before->seqno);
		c = parent->firstchild;
		if (!c)
			return;
		if (c == before) {
			parent->firstchild = add;
			add->sibling = before;
			goto ab;
		}
		while (c->sibling && c->sibling != before)
			c = c->sibling;
		if (!c->sibling)
			return;
		c->sibling = add;
		add->sibling = before;
		goto ab;
	}

/* type = a, appendchild */
	debugPrint(4, "linkage, %s %d linked into %s %d",
		   a_name, add->seqno, p_name, parent->seqno);
	if (!parent->firstchild)
		parent->firstchild = add;
	else {
		c = parent->firstchild;
		while (c->sibling)
			c = c->sibling;
		c->sibling = add;
	}

ab:
	add->parent = parent;
	add->deleted = false;

	t = add;
	debugPrint(4, "fixup %s %d", a_name, t->seqno);
	action = t->action;
	cx = t->f0->cx;
	t->name = get_property_string_0(cx, t->jv, "name");
	t->id = get_property_string_0(cx, t->jv, "id");
	t->jclass = get_property_string_0(cx, t->jv, "class");

	switch (action) {
	case TAGACT_INPUT:
		jst = get_property_string_0(cx, t->jv, "type");
		setTagAttr(t, "type", jst);
		t->value = get_property_string_0(cx, t->jv, "value");
		htmlInputHelper(t);
		break;

	case TAGACT_OPTION:
		if (!t->value)
			t->value = emptyString;
		if (!t->textval)
			t->textval = emptyString;
		break;

	case TAGACT_TA:
		t->action = TAGACT_INPUT;
		t->itype = INP_TA;
		t->value = get_property_string_0(cx, t->jv, "value");
		if (!t->value)
			t->value = emptyString;
// Need to create the side buffer here.
		formControl(t, true);
		break;

	case TAGACT_SELECT:
		t->action = TAGACT_INPUT;
		t->itype = INP_SELECT;
		if (typeof_property_0(cx, t->jv, "multiple"))
			t->multiple = true;
		formControl(t, true);
		break;

	case TAGACT_TR:
		t->controller = findOpenTag(t, TAGACT_TABLE);
		break;

	case TAGACT_TD:
		t->controller = findOpenTag(t, TAGACT_TR);
		break;

	}			/* switch */
}				/* javaSetsLinkage */

/* the new string, the result of the render operation */
static char *ns;
static int ns_l;
static bool invisible, tdfirst;
static Tag *inv2, *inv3;	// invisible via css
static int listnest;		/* count nested lists */
/* None of these tags nest, so it is reasonable to talk about
 * the current open tag. */
static Tag *currentForm, *currentA;

static char *backArrow(char *s)
{
	if (!s)
		s = ns + ns_l;
	while (s > ns) {
		if ((uchar) (*--s) == 0xe2 && (uchar) s[1] == 0x89 &&
		    ((uchar) s[2] == 0xaa || (uchar) s[2] == 0xab))
			return s;
	}
	return 0;
}

static char *backColon(char *s)
{
	while (s > ns)
		if (*--s == ':')
			break;
	return s;
}

static void swapArrow(void)
{
	char *s = ns + ns_l - 6;
	if (s > ns &&
	    !strncmp(s, "≫\0020", 5) && (s[5] == '>' || s[5] == '}')) {
		strmove(s, s + 3);
		strcpy(s + 3, "≫");
	}
}

static void tagInStream(int tagno)
{
	char buf[32];
	sprintf(buf, "%c%d*", InternalCodeChar, tagno);
	stringAndString(&ns, &ns_l, buf);
}				/* tagInStream */

/* see if a number or star is pending, waiting to be printed */
static void liCheck(Tag *t)
{
	Tag *ltag;	/* the list tag */
	if (listnest && (ltag = findOpenList(t)) && ltag->post) {
		char olbuf[32];
		if (ltag->ninp)
			tagInStream(ltag->ninp);
		if (ltag->action == TAGACT_OL) {
			int j = ++ltag->lic;
			sprintf(olbuf, "%d. ", j);
		} else {
			strcpy(olbuf, "* ");
		}
		if (!invisible)
			stringAndString(&ns, &ns_l, olbuf);
		ltag->post = false;
	}
}				/* liCheck */

static Tag *deltag;

static void renderNode(Tag *t, bool opentag)
{
	int tagno = t->seqno;
	Frame *f = t->f0;
	char hnum[40];		/* hidden number */
#define ns_hnum() stringAndString(&ns, &ns_l, hnum)
#define ns_ic() stringAndChar(&ns, &ns_l, InternalCodeChar)
	int j, l;
	int itype;		/* input type */
	const struct tagInfo *ti = t->info;
	int action = t->action;
	char c;
	bool endcolor;
	bool retainTag;
	const char *a;		/* usually an attribute */
	char *u;
	Tag *ltag;	/* list tag */

	debugPrint(6, "rend %c%s", (opentag ? ' ' : '/'), t->info->name);

	if (deltag) {
		if (t == deltag && !opentag)
			deltag = 0;
li_hide:
/* we can skate past the li tag, but still need to increment the count */
		if (action == TAGACT_LI && opentag &&
		    (ltag = findOpenList(t)) && ltag->action == TAGACT_OL)
			++ltag->lic;
		return;
	}
	if (t->deleted) {
		deltag = t;
		goto li_hide;
	}

	if (inv2) {
		if (inv2 == t)
			inv2 = NULL;
		return;
	}

	if (inv3 == t) {
		inv3 = NULL;
// I tried to remove an empty invisible section,
// but it's never really empty due to tag markers.
		stringAndString(&ns, &ns_l, "\r}'\r");
		return;
	}

	endcolor = false;
	if (doColors && !opentag && t->iscolor) {
		char *u0, *u1, *u3;
// don't put a color around whitespace
		u1 = backArrow(0);
// there should always be a previous color marker
		if (!u1)
			goto nocolorend;
		if ((uchar) u1[2] == 0xab)	// close
			goto yescolorend;
		for (u3 = u1 + 3; *u3; ++u3) {
			if (*u3 == InternalCodeChar) {
				for (++u3; isdigit(*u3); ++u3) ;
				if (*u3 == '*' && !*++u3)
					break;
			}
			if (!isspace(*u3))
				goto yescolorend;
		}
		u0 = backColon(u1);
		if (*u0 != ':')
			goto yescolorend;
		for (u3 = u0 + 1; u3 < u1; ++u3)
			if (*u3 != ' ' && !isalpha(*u3))
				goto yescolorend;
		u1 += 3;
		strmove(u0, u1);
		ns_l -= (u1 - u0);
		goto nocolorend;
yescolorend:
		stringAndString(&ns, &ns_l, "≫");
		endcolor = true;
	}
nocolorend:

	if (!opentag && ti->bits & TAG_NOSLASH)
		return;

	if (opentag && t->jv) {
// what is the visibility now?
		uchar v_now = 2;
		t->disval =
		    run_function_onearg(f, f->winobj, "eb$visible", t->jv);
		if (t->disval == 1)
			v_now = 1;
		if (t->disval == 2)
			v_now = 3;
		if (t->action == TAGACT_TEXT && v_now == 2) {
			Tag *y = t;
			while (y && y->f0 == f) {
				uchar dv = y->disval;
				if (dv == DIS_TRANSPARENT)
					v_now = 1;
				if (dv == DIS_HOVERCOLOR)
					v_now = 3;
				if (dv >= DIS_COLOR)
					break;
				y = y->parent;
			}
		}
// gather some stats
		if (v_now == 1)
			++invcount;
		if (v_now == 3)
			++hovcount;
		if (v_now == 1) {
			if (!showHover) {
				inv2 = t;
				return;
			}
			if (!inv3) {
				inv3 = t;
// merge adjacent invisible sections together
				if (ns_l >= 4
				    && stringEqual(ns + ns_l - 4, "\r}'\r"))
					ns_l -= 4;
				else
					stringAndString(&ns, &ns_l, "\r`{\r");
			}
		}
		if (!showHover && v_now == 3 && !activeBelow(t)) {
			inv2 = t;
			return;
		}
		if (action == TAGACT_TEXT && t->jv &&
		    get_property_bool(f, t->jv, "inj$css")) {
			++injcount;
			if (!showHover) {
				inv2 = t;
				return;
			}
		}
	}

	retainTag = true;
	if (invisible)
		retainTag = false;
	if (ti->bits & TAG_INVISIBLE) {
		retainTag = false;
		invisible = opentag;
/* special case for noscript with no js */
		if (action == TAGACT_NOSCRIPT && !f->cx)
			invisible = false;
	}

	if (doColors && opentag) {
		char *u0, *u1, *u2, *u3;
		jsobjtype so;	// style object
		char *color = 0, *recolor = 0;
		t->iscolor = false;
		so = get_property_object(f, t->jv, "style");
		if (so)
			color = get_property_string(f, so, "color");
		if (!color || !color[0])
			goto nocolor;
		caseShift(color, 'l');
		recolor = closeColor(color);
		if (!recolor) {
			nzFree(color);
			goto nocolor;
		}
		if (recolor != color)
			nzFree(color);
		if (stringEqual(recolor, "inherit")) {	// not a color
			nzFree(recolor);
			goto nocolor;
		}
// is this the same as the previous?
		u2 = backArrow(0);
		if (!u2)
			goto yescolor;
		if ((uchar) u2[2] == 0xaa) {	// open
			u1 = u2;
			u2 = 0;	// no closing
		} else {
			u1 = backArrow(u2);
			if (!u1 || (uchar) u1[2] != 0xaa)
				goto yescolor;
		}
// back up to :
		u0 = backColon(u1);
		if (*u0++ != ':' ||
		    u1 - u0 != strlen(recolor) || memcmp(u0, recolor, u1 - u0))
			goto yescolor;
		if (!u2) {
// it's the same color, orange inside orange
			nzFree(recolor);
			goto nocolor;
		}
// merge sections if there are no words in between
		for (u3 = u2; *u3; ++u3) {
			if (*u3 == InternalCodeChar)
				for (++u3; isdigit(*u3); ++u3) ;
			if (isalnum(*u3))
				goto yescolor;
		}
		strmove(u2, u2 + 3);
		ns_l -= 3;
		nzFree(recolor);
		t->iscolor = true;
		goto nocolor;
yescolor:
		stringAndChar(&ns, &ns_l, ':');
		stringAndString(&ns, &ns_l, recolor);
		stringAndString(&ns, &ns_l, "≪");
		nzFree(recolor);
		t->iscolor = true;
	}
nocolor:

	switch (action) {
	case TAGACT_TEXT:
		if (t->jv) {
// defer to the javascript text.
// either we query js every time, on every piece of text, as we do now,
// or we include a setter so that TextNode.data assignment has a side effect.
			char *u = get_property_string(f, t->jv, "data");
			if (u) {
				nzFree(t->textval);
				t->textval = u;
			}
		}
		if (!t->textval)
			break;
		liCheck(t);
		if (!invisible) {
			tagInStream(tagno);
			stringAndString(&ns, &ns_l, t->textval);
		}
		break;

	case TAGACT_A:
		liCheck(t);
		currentA = (opentag ? t : 0);
		if (!retainTag)
			break;
// Javascript might have set or changed this url.
		if (opentag && t->jv) {
			char *new_url = get_property_url(f, t->jv, false);
			if (new_url && *new_url) {
				nzFree(t->href);
				t->href = new_url;
			}
		}
		if (opentag && !t->href) {
// onclick turns this into a hyperlink.
			if (tagHandler(tagno, "onclick"))
				t->href = cloneString("#");
		}
		if (t->href) {
			if (opentag) {
				sprintf(hnum, "%c%d{", InternalCodeChar, tagno);
				if (t->jv
				    && (a =
					get_property_string(f, t->jv, "title"))) {
					++hovcount;
					if (showHover) {
						stringAndString(&ns, &ns_l, a);
						stringAndChar(&ns, &ns_l, ' ');
					}
					cnzFree(a);
				}
			} else
				sprintf(hnum, "%c0}", InternalCodeChar);
		} else {
			if (opentag)
				sprintf(hnum, "%c%d*", InternalCodeChar, tagno);
			else
				hnum[0] = 0;
		}
		ns_hnum();
		if (endcolor)
			swapArrow();
		break;

// check for span onclick and make it look like a link.
// Maybe we should do more than span, but just span for now.
	case TAGACT_SPAN:
// If nothing in the span then the title becomes important.
		a = 0, u = 0;
		if (!t->firstchild && opentag) {
			a = attribVal(t, "title");
			if (allowJS && t->jv)
				u = get_property_string(f, t->jv, "title");
		}
// If an onclick function, then turn this into a hyperlink, thus clickable.
// At least one site adds the onclick function via javascript, not html.
// But only at the start, so maybe we only need to check on the first render.
// But maybe some other site adds onclick later. Do we have to check every time?
// This rerender function is getting more and more js intensive!
		if (!t->onclick && t->jv && handlerPresent(f, t->jv, "onclick"))
			t->onclick = true;
		if (!t->onclick) {
// regular span
			if (u)
				stringAndString(&ns, &ns_l, u), nzFree(u);
			else if (a)
				stringAndString(&ns, &ns_l, a);
			goto nop;
		}
// this span has click, so turn into {text}
		if (opentag) {
			sprintf(hnum, "%c%d{", InternalCodeChar, tagno);
			ns_hnum();
			if (u)
				stringAndString(&ns, &ns_l, u), nzFree(u);
			else if (a)
				stringAndString(&ns, &ns_l, a);
		} else {
			sprintf(hnum, "%c0}", InternalCodeChar);
			ns_hnum();
			if (endcolor)
				swapArrow();
		}
		break;

	case TAGACT_OL:
	case TAGACT_UL:
		t->lic = t->slic;
		t->post = false;
		if (opentag)
			++listnest;
		else
			--listnest;
	case TAGACT_DL:
	case TAGACT_DT:
	case TAGACT_DD:
	case TAGACT_DIV:
	case TAGACT_OBJECT:
	case TAGACT_BR:
	case TAGACT_P:
	case TAGACT_H:
	case TAGACT_NOP:
nop:
		if (invisible)
			break;
		j = ti->para;
		if (opentag)
			j &= 3;
		else
			j >>= 2;
		if (j) {
			c = '\f';
			if (j == 1) {
				c = '\r';
				if (action == TAGACT_BR)
					c = '\n';
			}
			stringAndChar(&ns, &ns_l, c);
			if (doColors && t->iscolor &&
			    ns_l > 4 && !memcmp(ns + ns_l - 4, "≪", 3)) {
// move the newline before the color
				char *u0 = ns + ns_l - 4;
				u0 = backColon(u0);
				if (*u0 == ':') {
					int j = strlen(u0);
					memmove(u0 + 1, u0, j);
					*u0 = c;
				}
			}
			if (opentag && action == TAGACT_H) {
				strcpy(hnum, ti->name);
				strcat(hnum, " ");
				ns_hnum();
			}
		}
/* tags with id= have to be part of the screen, so you can jump to them */
		if (t->id && opentag && action != TAGACT_LI)
			tagInStream(tagno);
		break;

	case TAGACT_PRE:
		if (!retainTag)
			break;
/* one of those rare moments when I really need </tag> in the text stream */
		j = (opentag ? tagno : t->balance->seqno);
/* I need to manage the paragraph breaks here, rather than t->info->para,
 * which would rule if I simply redirected to nop.
 * But the order is wrong if I do that. */
		if (opentag)
			stringAndChar(&ns, &ns_l, '\f');
		sprintf(hnum, "%c%d*", InternalCodeChar, j);
		ns_hnum();
		if (!opentag)
			stringAndChar(&ns, &ns_l, '\f');
		break;

	case TAGACT_FORM:
		currentForm = (opentag ? t : 0);
		goto nop;

	case TAGACT_INPUT:
		if (!retainTag)
			break;
		if (!opentag) {
// button tag opens and closes, like anchor.
// Check and make sure it's not </select>
			if (!stringEqual(t->info->name, "button"))
				break;
// <button></button> with no text yields "push".
			while (ns_l && isspace(ns[ns_l - 1]))
				--ns_l;
			if (ns_l >= 3 && ns[ns_l - 1] == '<'
			    && isdigit(ns[ns_l - 2]))
				stringAndString(&ns, &ns_l,
						i_getString(MSG_Push));
			ns_ic();
			stringAndString(&ns, &ns_l, "0>");
			if (endcolor)
				swapArrow();
			break;
		}
// value has to be something.
		if (!t->value)
			t->value = emptyString;
		itype = t->itype;
		if (itype == INP_HIDDEN)
			break;
		liCheck(t);
		if (itype == INP_TA) {
			j = t->lic;
			if (j)
				sprintf(hnum, "%c%d<buffer %d%c0>",
					InternalCodeChar, t->seqno, j,
					InternalCodeChar);
			else if (t->value[0])
				sprintf(hnum, "%c%d<buffer text%c0>",
					InternalCodeChar, t->seqno,
					InternalCodeChar);
			else
				sprintf(hnum, "%c%d<buffer ?%c0>",
					InternalCodeChar, t->seqno,
					InternalCodeChar);
			ns_hnum();
			break;
		}
		sprintf(hnum, "%c%d<", InternalCodeChar, tagno);
		ns_hnum();
// button stops here, until </button>
		if (stringEqual(t->info->name, "button"))
			break;
		if (itype < INP_RADIO) {
			if (t->value[0])
				stringAndString(&ns, &ns_l, t->value);
			else if (itype == INP_SUBMIT || itype == INP_IMAGE)
				stringAndString(&ns, &ns_l,
						i_getString(MSG_Submit));
			else if (itype == INP_RESET)
				stringAndString(&ns, &ns_l,
						i_getString(MSG_Reset));
			else if (itype == INP_BUTTON)
				stringAndString(&ns, &ns_l,
						i_getString(MSG_Push));
		} else {
// in case js checked or unchecked
			if (allowJS && t->jv)
				t->checked =
				    get_property_bool(f, t->jv, "checked");
			stringAndChar(&ns, &ns_l, (t->checked ? '+' : '-'));
		}
		if (currentForm && (itype == INP_SUBMIT || itype == INP_IMAGE)) {
			if (currentForm->secure)
				stringAndString(&ns, &ns_l,
						i_getString(MSG_Secure));
			if (currentForm->bymail)
				stringAndString(&ns, &ns_l,
						i_getString(MSG_Bymail));
		}
		ns_ic();
		stringAndString(&ns, &ns_l, "0>");
		break;

	case TAGACT_LI:
		if ((ltag = findOpenList(t))) {
			ltag->post = true;
/* borrow ninp to store the tag number of <li> */
			ltag->ninp = t->seqno;
		}
		goto nop;

	case TAGACT_HR:
		liCheck(t);
		if (retainTag) {
			tagInStream(tagno);
			stringAndString(&ns, &ns_l, "\r----------\r");
		}
		break;

	case TAGACT_TR:
		if (opentag)
			tdfirst = true;
	case TAGACT_TABLE:
		goto nop;

	case TAGACT_TD:
		if (!retainTag)
			break;
		if (tdfirst)
			tdfirst = false;
		else {
			liCheck(t);
			j = ns_l;
			while (j && ns[j - 1] == ' ')
				--j;
			ns[j] = 0;
			ns_l = j;
			stringAndChar(&ns, &ns_l, TableCellChar);
		}
		tagInStream(tagno);
		break;

/* This is strictly for rendering math pages written with my particular css.
* <span class=sup> becomes TAGACT_SUP, which means superscript.
* sub is subscript and ovb is overbar.
* Sorry to put my little quirks into this program, but hey,
* it's my program. */
	case TAGACT_SUP:
	case TAGACT_SUB:
	case TAGACT_OVB:
		if (!retainTag)
			break;
		if (action == TAGACT_SUB)
			j = 1;
		if (action == TAGACT_SUP)
			j = 2;
		if (action == TAGACT_OVB)
			j = 3;
		if (opentag) {
			static const char *openstring[] = { 0,
				"[", "^(", "`"
			};
			t->lic = ns_l;
			liCheck(t);
			stringAndString(&ns, &ns_l, openstring[j]);
			break;
		}
		if (j == 3) {
			stringAndChar(&ns, &ns_l, '\'');
			break;
		}
/* backup, and see if we can get rid of the parentheses or brackets */
		l = t->lic + j;
		u = ns + l;
/* skip past <span> tag indicator */
		if (*u == InternalCodeChar) {
			++u;
			while (isdigit(*u))
				++u;
			++u;
		}
		if (j == 2 && isalphaByte(u[0]) && !u[1])
			goto unparen;
		if (j == 2 && (stringEqual(u, "th") || stringEqual(u, "rd")
			       || stringEqual(u, "nd") || stringEqual(u, "st"))) {
			strmove(ns + l - 2, ns + l);
			ns_l -= 2;
			break;
		}
		while (isdigitByte(*u))
			++u;
		if (!*u)
			goto unparen;
		stringAndChar(&ns, &ns_l, (j == 2 ? ')' : ']'));
		break;
unparen:
/* ok, we can trash the original ( or [ */
		l = t->lic + j;
		strmove(ns + l - 1, ns + l);
		--ns_l;
		if (j == 2)
			stringAndChar(&ns, &ns_l, ' ');
		break;

	case TAGACT_AREA:
	case TAGACT_FRAME:
		if (!retainTag)
			break;

		if (t->f1 && !t->contracted) {	/* expanded frame */
			sprintf(hnum, "\r%c%d*%s\r", InternalCodeChar, tagno,
				(opentag ? "`--" : "--`"));
			ns_hnum();
			break;
		}

/* back to unexpanded frame or area */
		if (!opentag)
			break;
		liCheck(t);
		stringAndString(&ns, &ns_l,
				(action == TAGACT_FRAME ? "\rFrame " : "\r"));
		a = 0;
		if (action == TAGACT_AREA)
			a = attribVal(t, "alt");
		u = (char *)a;
		if (!u) {
			u = t->name;
			if (!u)
				u = altText(t->href);
		}
		if (!u)
			u = (action == TAGACT_FRAME ? "???" : "area");
		if (t->href) {
			sprintf(hnum, "%c%d{", InternalCodeChar, tagno);
			ns_hnum();
		}
		if (t->href || action == TAGACT_FRAME)
			stringAndString(&ns, &ns_l, u);
		if (t->href) {
			ns_ic();
			stringAndString(&ns, &ns_l, "0}");
		}
		stringAndChar(&ns, &ns_l, '\r');
		if (t->f1 && t->contracted)	/* contracted frame */
			deltag = t;
		break;

	case TAGACT_MUSIC:
		liCheck(t);
		if (!retainTag)
			break;
		if (!t->href)
			break;
		sprintf(hnum, "\r%c%d{", InternalCodeChar, tagno);
		ns_hnum();
		stringAndString(&ns, &ns_l,
				(ti->name[0] ==
				 'b' ? "Background Music" : "Audio passage"));
		sprintf(hnum, "%c0}\r", InternalCodeChar);
		ns_hnum();
		break;

	case TAGACT_IMAGE:
		liCheck(t);
		tagInStream(tagno);
		if (!currentA) {
			if ((a = attribVal(t, "alt"))) {
				u = altText(a);
				a = NULL;
/* see if js has changed the alt tag */
				if (allowJS && t->jv) {
					char *aa =
					    get_property_string(f, t->jv, "alt");
					if (aa)
						u = altText(aa);
					nzFree(aa);
				}
				if (u && !invisible) {
					stringAndChar(&ns, &ns_l, '[');
					stringAndString(&ns, &ns_l, u);
					stringAndChar(&ns, &ns_l, ']');
				}
			}
			break;
		}
/* image is part of a hyperlink */
		if (!retainTag || !currentA->href || currentA->textin)
			break;
		u = 0;
		a = attribVal(t, "alt");
		if (a)
			u = altText(a);
		if (!u)
			u = altText(t->name);
		if (!u)
			u = altText(currentA->href);
		if (!u)
			u = altText(t->href);
		if (!u)
			u = "image";
		stringAndString(&ns, &ns_l, u);
		break;

	}			/* switch */
}				/* renderNode */

/* returns an allocated string */
char *render(int start)
{
	Frame *f;
	for (f = &cw->f0; f; f = f->next)
		if (f->cx)
			set_property_bool(f, f->winobj, "rr$start", true);
	ns = initString(&ns_l);
	invisible = false;
	inv2 = inv3 = NULL;
	listnest = 0;
	currentForm = currentA = NULL;
	traverse_callback = renderNode;
	traverseAll(start);
	return ns;
}				/* render */

// Create buffers for text areas, so the user can type in comments or whatever
// and send them to the website in a fill-out form.
void itext(void)
{
	int ln = cw->dot;	// line number
	pst p;			// the raw line to scan
	int n;
	Tag *t;
	char newtext[20];
	bool change = false, inp = false;

	p = fetchLine(ln, -1);
	while (*p != '\n') {
		if (*p != InternalCodeChar) {
			++p;
			continue;
		}
		n = strtol((char *)p + 1, (char **)&p, 10);
		if (*p != '<')
			continue;
		inp = true;
		t = tagList[n];
		if (t->itype != INP_TA || t->lic)
			continue;
		t->lic = sideBuffer(0, t->value, -1, 0);
		change = true;
		sprintf(newtext, "buffer %d", t->lic);
// updateFieldInBuffer is crazy inefficient in that it searches through the
// whole buffer, and we know it's on the current line, but really, how often
// do you invoke this command?
		updateFieldInBuffer(n, newtext, false, false);
// And now all the pointers are invalid so break out.
// If there's another textarea on the same line you have to issue the command
// again, but really, how often does that happen?
		break;
	}

	if (change)
		displayLine(ln);
	else if (inp)
		i_puts(MSG_NoChange);
	else
		i_puts(MSG_NoInputFields);
}
