/* html.c, parse and render html. */

#include "eb.h"

#ifdef _MSC_VER			// sleep(secs) macro
#define SLEEP(a) Sleep(a * 1000)
extern int gettimeofday(struct timeval *tp, void *tzp);
#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__) || defined(__OpenBSD__)
#define pthread_tryjoin_np pthread_join
#endif

uchar browseLocal;
bool showall, textd = true, doColors;

static Tag *js_reset, *js_submit;
static const int asyncTimer = 700;

// start a document.write on current frame, i.e. cf
void dwStart(void)
{
	if (cf->dw)
		return;
	cf->dw = initString(&cf->dw_l);
	stringAndString(&cf->dw, &cf->dw_l, "<body>");
}

bool handlerPresent(const Tag *t, const char *name)
{
	const char *name2 = tack_array(name);
	return (typeof_property_t(t, name) == EJ_PROP_FUNCTION ||
            typeof_property_t(t, name) == EJ_PROP_STRING ||
            (name2 && typeof_property_t(t, name2) == EJ_PROP_ARRAY));
}

// called with click, dblclick, reset, or submit
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->jslink)
		return false;
	if (!isJSAlive)
		return false;
	if (!handlerPresent(t, 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;
}

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");
}

static bool inputDisabled(const Tag *t);
static bool inputHidden(const Tag *t);
static bool inputReadonly(const Tag *t);

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 (!controlledBy(t, sel)) continue;
		if (!(s = t->textval)) continue;
		if(inputHidden(t)) continue;
		if (stringEqualCI(s, name)) {
			em = t;
			continue;
		}
		if (exact)
			continue;
		if (strcasestr(s, name)) {
			pm = t;
			++pmcount;
		}
	}
	if (em)
		return em;
	if (pmcount == 1)
		return pm;
	*pmc = (pmcount > 0);
	return 0;
}

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

	for (t = cw->optlist; t; t = t->same) {
		if (!controlledBy(t, sel)) continue;
		if (!t->textval) continue;
		++cnt;
		if(inputHidden(t)) continue;
		if (cnt == n) return t;
	}
	return 0;
}

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, val_count;
	int len = strlen(input);
	int n, pmc;
	const char *s, *e;	/* start and end of an option */
	char *iopt;		/* individual option */

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

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

	s = input;
	while (*s) {
		e = 0;
		if (sel->multiple)
			e = strchr(s, selsep);
		if (!e)
			e = s + strlen(s);
		len = e - s;
		strncpy(iopt, s, len);
		iopt[len] = 0;
		s = e;
		if (*s == selsep)
			++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(inputDisabled(t) ||
		inputDisabled(sel) ||
		(t->parent && t->parent->action == TAGACT_OPTG && inputDisabled(t->parent))) {
// If we are gathering values, to submit the form,
// then somehow all these options have been selected.
// The user can't select a disabled option but somebody did,
// perhaps js, so let it ride.
			if(val_p) continue;
			setError(MSG_Disabled);
			goto fail;
		}

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

		if (disp_p) {
			if (*disp)
				stringAndChar(&disp, &disp_l, selsep);
// special exception for <input> with datalist - revert back to value.
			stringAndString(&disp, &disp_l,
			(sel->action == TAGACT_DATAL ? t->value : t->textval));
		}

		if (setcheck) {
			t->checked = true;
			if (t->jslink && allowJS) {
				set_property_bool_t(t, "selected", true);
				if (sel->jslink && allowJS)
					set_property_number_t(sel, "selectedIndex", t->lic);
			}
		}
	}			// loop over multiple options

	if (val_p)
		*val_p = val_count ? val : 0;
	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;
}

/*********************************************************************
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, const Tag *active)
{
    Tag *t;
    int itype, j, cx;
    char *value, *cxbuf;

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

    if(active)
        set_property_object_doc(cf, "activeElement", active);

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

        if (itype >= INP_RADIO) {
            int checked = fieldIsChecked(t->seqno);
            if (checked < 0)
                continue;
            t->checked = checked;
            set_property_bool_t(t, "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);
            run_function_bool_t(t, "eb$bso");
            if (value) {
                nzFree(t->value);
                t->value = value;
            }
            continue;
        }

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

        if (value) {
            set_property_string_t(t, "value", value);
            nzFree(t->value);
            t->value = value;
        }
    } // loop over tags

    debugPrint(4, "jSyncup ends");
}

void jClearSync(void)
{
	if (cw->browseMode) {
		cw->sank = false;
		debugPrint(4, "clear sync");
		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
}

/*********************************************************************
Some notes on the newlocation system.
javascript is not reentrant, nor the machinery that I build around it.
If js tells us to replace a window with another, like location = new_url,
we can't fetch and parse and run that window from within js.
After all, the new window has its own js, in a new context.
Instead we set a global variable, newlocation, and when js returns,
we test newlocation to see if we should open a new window.
It's awkward, as you will see, but we don't have much choice.
There is a suite of global variables here.
newloc_d a delay factor, open the window after so many seconds.
This is used by <meta http-equiv="refresh" content="delay;url(blah)">
newloc_r boolean do we refresh the page or open a new window?
newloc_f frame that is being replaced or pushed.
in http.c:
frameExpandLine clears newlocation, but it should be clear already.
reexpandFrame uses the newloc variables.
cf, the current frame, is saved, and restored when reexpandFrame is finished.
reexpandFrame sets cf to newloc_f, running in that frame.
newloc_r is assumed to be true, so call it that way.
That frame page is deleted and a new one fetched and processed.
newlocation is cleared, so we don't keep doing this in an infinite loop.
in html.c:
static void gotoLocation(char *url, int delay, bool rf, Frame *f)...
url is allocated and if it sticks, it is passed to newlocation,
which is then responsible for that string.
newlocation is always freed, in some way or another, when it is used or cleared.
If it doesn't stick, then url is freed.
It won't stick if we already have a newlocation set,
with the same or lesser delay.
We print debug  messages at level 1 because you should probably see that.
So this is, from the get-go, not the best design.
If js causes two separate frames to replace themselves, only the first one
will work. But you know, I don't think this ever happens in the wild.
These redirections are very rare, and there's usually only one,
so this is the design for now.
In any case there are debug messages along the way to help.
	newloc_d = delay, newloc_r = rf, newloc_f = f;
This is the only function in html.c that accesses newlocation,
so who calls gotoLocation?
htmlMetaHelper, as described above.
runScriptsPending if a script calls form.submit.
The new url which is the action of the form, no refresh, no delay,
and frame is the frame that contains the form.
void domOpensWindow(const char *href, const char *name)...
href starts with r or p, refresh the existing buffer
or push it onto the stack and open a new window.
Then an integer which is the context number for the frame.
This is turned into a frame pointer, and that should always work.
Finally the url, which becomes newlocation.
These variables are passed to gotoLocation, and when js is done
we can take action based on newlocation and its friends.
name is the name of the new window.
href could be a relative url.
It is resolved against the base of cf, the running frame.
A subframe can't push a new window.
The master frame, that controls the entire window, can push onto the stack
and then open a new window, that happens whenever you go to a hyperlink.
That is ok; but not a subframe.
So if newloc_r is false, and if f is not the top frame,
there are two choices.
We could still be parsing the html for that frame.
Use the document.write system to add this to the document as a hyperlink.
new Window(blah) now becomes a suggestion rather than a directive.
It is a hyperlink, go to it if you want.
In fact we can do this whenever we are still in the browse process,
for a subframe or for the top frame.
Let's set that to the side for now.
So we have already browsed the page,
this action coming from a timer or onclick or something dynamic.
If you want a new page it has to be the top frame.
A subframe prints a message and returns.
Now we have a frame replacement or pushing the entire window
and opening a new one.
This is when we call gotoLocation and set the newloc variables.
domOpensWindow is only called from the native method that corresponds to eb$newLocation.
This happens under two circumstances.
eb$newLocation called from the Window constructor new Window(blah)
that opens a new window and is not refresh
The first letter of href is p indicating push.
eb$newLocation called from url_hrefset,
for location = blah or document.location = blah.
The first letter of href is r, this is a replacement.
in buffers.c:
BrowseCurrentBuffer clears newlocation, but it should be clear already.
Just a precaution before we browse.
Browsing could set newlocation, by <meta http-equiv>.
Other mechanisms turn into hyperlinks.
Well we check newlocation after browse, in fact we check it after any command
that might invoke js.
Like pushing a button; after infPush(), check for newlocation.
This is a theme throughout runCommand().
If newlocation is set, goto redirect;
What happens there?
Set cf to the top frame.
If newloc_f is a subframe, goto replaceframe.
If refresh was false, if we were pushing a new page, on a frame
other than the top frame, we would print a message and not set newlocation.
We already talked about this.
So at replaceframe we know newloc_r is true.
Call reexpandFrame.
Check again for newlocation, it could happen, but guard against an
infinite fetch loop.
Alternatively, the frame could be the top frame.
At that point, create a new browse command, as though you had typed it in.
The browse command will include nostack if new_r is true.
And on through runCommand().
There is another situation - a timer fires and sets newlocation,
not in response to any of our commands.
We are in inputLine(), gathering input.
First, debug print a message, which you might not see
Check to see if this is from a background window.
It shouldn't be, timers only run on the foreground window.
Just in case, print an error message and clear newlocation.
if this was the result of a command.
It just happened; maybe you should know what is happening.
Now here is the same logic. If we are pushing a page, it is the master frame.
Return a browse command, b url, as though the user had typed it in.
If new_r is true, return a special code ReF@b url.
It's special in that a user would never type it by accident.
runCommand sees this.
If it is the master frame, then run the b command as usual,
with nostack, so the window is replaced.
If it is a subframe, goto  replaceframe;
And that's how it works to date.
*********************************************************************/

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, Frame *f)
{
	if (!allowRedirection) {
		debugPrint(1, "javascript redirection disabled: %s", url);
		nzFree(url);
		return;
	}
	if (newlocation && delay >= newloc_d) {
		debugPrint(1, "redirection %s ignored", url);
		nzFree(url);
		return;
	}
	if(newlocation)
		debugPrint(1, "redirection %s displaced by %s", newlocation, url);
	else debugPrint(3, "redirection set %s", url);
	nzFree(newlocation), newlocation = url;
	newloc_d = delay, newloc_r = rf, newloc_f = f;
	if (!delay) js_redirects = true;
}

/* helper function for meta tag */
void htmlMetaHelper(const Tag *t)
{
	char *name;
	const char *content, *heq, *charset;
	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;

// if multiple charsets, the first one wins
	charset = attribVal(t, "charset");
	if(charset && *charset && !cf->charset)
		cf->charset = charset;

	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
		&& ! cw->imapMode3 && !ismc
		    && stringEqualCI(heq, "Refresh")) {
			if (parseRefresh(copy, &delay)) {
				char *newcontent;
				unpercentHost(copy);
				newcontent = resolveURL(cf->hbase, copy);
				gotoLocation(newcontent, delay, true, cf);
			}
		}
	}

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

	nzFree(copy);
}

static void debugGenerated(const char *h)
{
	const char *h1, *h2;
	h1 = strstr(h, "<body>"); // it should be there
	h1 = h1 ? h1 + 6 : h;
// and </body> should be at the end
	h2 = h + strlen(h) - 7;
// Yeah this is one of those times I override const, but I put it back,
// so it's like a const string.
	*(char*)h2 = 0;
	if(debugLevel == 3) {
		if(strlen(h1) >= 200)
			debugPrint(3, "Generated ↑long↑");
		else
			debugPrint(3, "Generated ↑%s↑", h1);
	} else
		debugPrint(4, "Generated ↑%s↑", h1);
	*(char*)h2 = '<';
}

static void runGeneratedHtml(Tag *t, const char *h)
{
	debugPrint(3, "parse html from docwrite");
	if (t)
		debugPrint(4, "parse under %s %d", t->info->name, t->seqno);
	else
		debugPrint(4, "parse under top");
	debugGenerated(h);
	const int startpos = htmlScanner(h, t, true);
	prerender(startpos);
	decorate(startpos, 0);
	debugPrint(3, "end parse html from docwrite");
}

// acid3 test[0] says we don't process a css file if it's content type is
// text/html. Should I test for anything outside of text/css?
bool cssCompatibleContent(const char *s)
{
	if (s && *s &&
	    !stringEqual(s, "text/css") &&
	    !stringEqual(s, "text/plain")) {
		debugPrint(3,   "css suppressed because content type is %s", s);
		return false;
	}
	return true;
}

// acid says a frame has to be text/html, not even text/plain.
// I don't know about xml.
bool frameCompatibleContent(const char *s)
{
	if (s && *s &&
	    !stringEqual(s, "text/html") &&
	    !stringEqual(s, "text/xml")) {
		debugPrint(3,   "frame suppressed because content type is %s", s);
		return false;
	}
	return true;
}

/*********************************************************************
Here is a load function, to load the data for javascript or css,
from the internet or from a local file, and possibly in a background thread.
Tag steps are as follows.
1 parsed as html
2 decorated with a coresponding javascript object
3 downloading in background
4 data fetched and in the js world and possibly deminimized
5 script has run
6 script could not run

Here are some notes on threads.
Particularly relevant for tags in step 3, downloading in the background.
This assumes curl is threadsafe, and we have configured it
so that different threads can perform different downloads, or actions,
and all share certain structures like the cookie jar.

* main.c, signal handler for ^c. This one is drastic!
If js has us in an infinite loop that has lasted fore 45 seconds,
spin off a new interactive (foreground) thread and kill the current thread.
js is turned off, and best not to turn it back on,
it was left in a strange state, and quickjs is not reentrant.
In fact you shouldn't do anything except save some critical files
you were working on, and exit.

* Download a file in background. Data is going to the file and not
associated with the foreground thread.
The decision to download could happen after we started reading the file.
Based on content-type in the http header, for example.
If this happens, stop the download.
From within the callback handler:
if (g->down_state == 5) return -1;
Now nobody is downloading the file, but hey we didn't get very far.
Copy i_get onto a static structure, not on anybody's stack.
Spin off a new thread that points to this structure.
Before that runs, the foreground thread could return, and its i_get goes away.
Child thread copies i_get onto its stack, and runs httpConnect anew.
This is managed by httpConnectBack1, conect in background.
The first function to manage a background connect.
There will be two more.
So the file just downloads by child thread, and finishes,
and prints a success message, and records the success for the bglist command,
and has no interaction with the foreground thread.
In fact you can run this test wherein the foreground window
completely goes away, and the child marches on.
edbrowse '' local-file
bg+
db3
e http://some-large-file
q
httpConnectBack1 is invoked two more times but it's really the same thing.
ftp download or gopher download and the user wants it in the background.
Download in background threads based on the toggle command bg.
Subsequent downloads are managed by the toggle command jsbg:
javascript or xhr files downloaded in background.
These will use httpConnectBack2 and httpConnectBack3.

* Download javascript by background threads.
This uses httpConnectBack2(), from loadScriptData().
The tag is passsed to the child thread and the tag must survive
during the entire download.
httpConnectBack2 makes its own i_get structure on its stack.
When parsing html, if js is enabled,
jsNode calls loadScriptData on each script tag.
This reads the file if it is local, or fetches it if jsbg is off,
or spawns the fetch thread if jsbg is true.
In the browse process, decorate is followed by run ScriptsPending.
This function runs the scripts in the html document, or at least,
makes sure they are loaded.
If we type q or ^ immediately after browse, the window goes away,
and we don't want scripts loading into tags that aren't even there.
If there is some corner case where that happens, freeTag has a defense against
it, which is described below.
If a script is created after browse, and rooted, i.e. put into the tree,
and if it is asynchronous, it is prepared, which starts the download process,
and then put on a timer.
It can run whenever it is loaded.
If the window closes while it is loading, freeTag sends an interrupt
signal to the child thread, then waits for it to finish,
which should be almost immediate thanks to the signal.
With the thread gone, it is safe to free the tag.

* Spawn threads to download the css files in background, just like the
javascript files, and using httpConnectBack2.
Thus loadScriptData processes script tags and link tags.

* Asynchronous xhr.
A thread is created to start the fetch of the data,
and it is put on a timer.
The timer watches, and when the thread is done, and the data is available,
it runs the javascript callback function on the data.
If the window closes while the xhr data is being read,
freeTag kills the thread, and waits for it to exit,
as described earlier.
*********************************************************************/

void loadScriptData(Tag *t)
{
	bool is_js = (t->action == TAGACT_SCRIPT);
	const char *sourcefile = "generated";
	char *sourcetext = 0;
	const char *filepart;
	char *b;
	int blen, n;
	Frame *f = t->f0;
	const char *altsource, *realsource;
	bool from_data = false;
	bool jsbg = down_jsbg;
	struct i_get g;

	if(cf->xmlMode) {
// I don't believe we load or run scripts under xml.
// If we load them, but don't run them, then this has to change somewhat.
// step = 5 means the script loaded and ran.
		t->step = 5;
		return;
	}

// If this tag is under <template>, and we clone it again and again,
// we could be asked to prepare it again and again.
	if((n = get_property_number_t(t, "eb$step")) > 0) {
		t->step = n;
		return;
	}

	if (intFlag) goto fail;

	if(!is_js) {
// docs do not say type or rel are spilldown, but things are easier if they are.
// See cssGather in shared.js. So I check for them as html tag attributes,
// know they will be added by setAttribute(), and do the spilldown here.
// This is probably not the best way - what if <link> is added dynamically.
		const char *a1 = attribVal(t, "type");
		const char *a2 = attribVal(t, "rel");
		if (a1)
			set_property_string_t(t, "type", a1);
		if (a2)
			set_property_string_t(t, "rel", a2);
// invalid type - no need to fetch
		if (!stringEqualCI(a1, "text/css") &&
		    !stringEqualCI(a2, "stylesheet"))
			goto fail;
	}

	if (f->fileName && !t->scriptgen)
		sourcefile = f->fileName;

	if (!t->href && t->jslink) {
// js might have set the source url.
		char *new_url = get_property_url_t(t, 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;
		}
	}

	if (t->href) {		/* fetch the javascript page */
		altsource = realsource = 0;
		if(is_js) {
			if (!javaOK(t->href))
				goto fail;
			from_data = isDataURI(t->href);
		}
		if (!from_data) {
			altsource = fetchReplace(t->href);
			realsource = (altsource ? altsource : t->href);
		}
		debugPrint(3, "%s source %s",
			(is_js ? "js" : "css"),
			   !from_data ? realsource : "data URI");

		if (from_data) {
			char *mediatype;
			int data_l = 0;
			if (parseDataURI(t->href, &mediatype,
					 &sourcetext, &data_l)) {
				prepareForBrowse(sourcetext, data_l);
				nzFree(mediatype);
			} else {
				debugPrint(3,
					   "Unable to parse data URI containing JavaScript");
				goto fail;
			}
		} else if ((browseLocal || altsource) && !isURL(realsource)) {
			char *h = cloneString(realsource);
			unpercentString(h);
			if (!fileIntoMemory(h, &b, &blen, 0)) {
				if (debugLevel >= 1)
					i_printf(is_js ? MSG_GetLocalJS : MSG_GetLocalCSS);
				nzFree(h);
				goto fail;
			}
			sourcetext = force_utf8(b, blen);
			if (!sourcetext)
				sourcetext = b;
			else
				nzFree(b);
			nzFree(h);
		} else {
// This has to happen before threads spin off, that is to say, in the
// foreground thread. We can't let httpConnect do it in a background thread.
			if (!curlActive) {
				eb_curl_global_init();
				cookiesFromJar();
				setupEdbrowseCache();
			}

// don't background fetch for xml, because the scripts never run
// and we can't guarantee to complete the fetch.
			if(cf->xmlMode) jsbg = false;

// async and defer are the same, as far is edbrowse is concerned.
// If neither is set we need the script right now.
// However, we don't execute it now, and we should.
// We need to implement that some day - but it scares me.
			if(is_js) {
				if(!t->async)
					t->async = (get_property_bool_t(t, "async") || get_property_bool_t(t, "defer"));
				if(!t->async) jsbg = false;
			}

			if(jsbg) {
// We can't background fetch if this is under <template>
				const Tag *u;
				for(u = t; u; u = u->parent)
					if(u->action == TAGACT_TEMPLATE ||
					u->action == TAGACT_HTML ||
					u->action == TAGACT_FRAME)
						break;
				if(u && u->action == TAGACT_TEMPLATE)
					jsbg = false;
			}

			if (jsbg && !demin && !uvw
			    && !pthread_create(&t->loadthread, NULL,
					       httpConnectBack2, (void *)t)) {
				t->threadcreated = true;
				t->js_ln = 1;
				sourcefile = realsource;
				filepart = getFileURL(sourcefile, true);
				t->js_file = cloneString(filepart);
// stop here and wait for the child process to download
				t->step = 3;
				return;
			}

// foreground fetch
			memset(&g, 0, sizeof(g));
			g.thisfile = f->fileName;
			g.uriEncoded = true;
			g.url = realsource;
			if (!httpConnect(&g)) {
				if (debugLevel >= 3)
					i_printf(is_js ? MSG_GetJS2 : MSG_GetCSS2);
				goto fail;
			}
			nzFree(g.cfn);
			nzFree(g.referrer);
			if (g.code == 200) {
				sourcetext = force_utf8(g.buffer, g.length);
				if (!sourcetext)
					sourcetext = g.buffer;
				else
					nzFree(g.buffer);
				if (!is_js && !cssCompatibleContent(g.content))
					cnzFree0(sourcetext);
			} else {
				nzFree(g.buffer);
				if (debugLevel >= 3)
					i_printf(is_js ? MSG_GetJS : MSG_GetCSS, g.url, g.code);
				goto fail;
			}
		}
		t->js_ln = 1;
		sourcefile = (!from_data ? realsource : "data_URI");
	} else {
		sourcetext = t->textval;
		t->textval = 0;
	}

	if (!sourcetext) {
// No text from the html, nor fetched from the internet.
// But it may be supplied by javascript, so step ahead, as if loaded.
// Note that this text cannot be deminimized.
		goto success;
	}

	set_property_string_t(t, is_js ? "text" : "css$data", sourcetext);
	nzFree(sourcetext);
	filepart = getFileURL(sourcefile, true);
	t->js_file = cloneString(filepart);
	if(!is_js) {
// indicate we can run onload, if there is one.
		t->lic = 1;
		goto success;
	}

// deminimize the code if we're debugging.
	if (demin)
		run_function_onearg_win(f, "eb$demin", t);
	if (uvw)
		run_function_onearg_win(f, "eb$watch", t);

success:
	t->step = 4;
	set_property_number_t(t, "eb$step", 4);
	return;

fail:
	t->step = 6;
}

// Like the above, but for link tags. Make sure they are all loaded.
void loadFinishCSS(void)
{
	Tag *t;
	for (t = cw->linklist; t; t = t->same) {
		if (t->dead || !t->jslink || t->step != 3)
			continue;
		if(cf != t->f0) continue; // some other frame
// don't need css until it is linked into the tree.
		if(!isRooted(t)) continue;
		if (intFlag) {
			t->step = 6;
			continue;
		}
		pthread_join(t->loadthread, NULL);
		t->threadjoined = true;
		if (!t->loadsuccess || t->hcode != 200) {
			if (debugLevel >= 3)
				i_printf(MSG_GetCSS, t->href, t->hcode);
			t->step = 6;
			continue;
		}
		set_property_string_t(t, "css$data", t->value);
		nzFree0(t->value);
		t->step = 4;	// loaded
		set_property_number_t(t, "eb$step", 4);
	}
}

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.
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 note 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.
See jsRunData() for executing the script and then its onload handler.

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 quickjs 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.
defered scripts are treated the same as 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 set readyState to complete,
then run all the onnload functions, after the synchronous 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.
The function isRooted(t) determines whether t is rooted;
this is needed in several places, besides running scripts.

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.
*********************************************************************/

bool isRooted(const Tag *t)
{
	const Tag *up;
	for(up = t; up; up = up->parent) {
		if(up->action == TAGACT_HEAD || up->action == TAGACT_BODY)
			return true;
		if(up->action == TAGACT_TEMPLATE)
			return false;
	}
	return false;
}

void runScriptNow(Frame *runframe, Tag *t)
{
    const char *a; // for attribute
    const char *gc_name; // garbage collection name
    const char *sourcefile;
    int ln; // line number
    bool is_module;
    t->step = 5; // now running the script
    set_property_number_t(t, "eb$step", 5);

    if (t->inxhr) {
// xhr looks like an asynchronous script.
        run_function_bool_t(t, "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 the object if it wants to
        gc_name = get_property_string_t(t, "backlink");
        if (gc_name) delete_property_win(cf, gc_name);
        cnzFree(gc_name);
        return;
    }

// If no language is specified, javascript is default.
    a = get_property_string_t(t, "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);
        return;
    }
    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_t(t, "type");
// allow for type 5e5857709a179301c738ca91-text/javascript, which really happens.
// Also application/javascript.
    is_module = (a && *a && stringEqualCI(a, "module"));
    if (!is_module && a && *a && !stringEqualCI(a, "javascript") &&
        ((ln = strlen(a)) < 11 || !stringEqualCI(a + ln - 11, "/javascript"))
    ) {
        debugPrint(3, "script tag %d type %s not executed", t->seqno, a);
        cnzFree(a);
        return;
    }
    cnzFree(a);

     sourcefile = t->js_file;
    if (!sourcefile) sourcefile = "generated";
    ln = t->js_ln;
    if (!ln) ln = 1;
    if (cf != runframe)
        debugPrint(4, "running script at a lower frame %s", sourcefile);
    debugPrint(3, "exec %s at %d", sourcefile, ln);
    jsRunData(t, sourcefile, ln, is_module);
    debugPrint(3, "exec complete");

// Last step, look for document.write from this script
    if (!cf->dw) return;
// completely different behavior before and after browse
// After browse, it clobbers the page.
    if(cf->browseMode && ! cf->dw_clobber) {
        debugPrint(3, "document.write clobber 1");
        run_function_onestring_t(cf->bodytag, "eb$dbih",
            strstr(cf->dw, "<body>")+6);
		cf->dw_clobber = true;
    } else {
// Any newly generated scripts have to run next. Move them up in the linked list.
        Tag *t1, *t2, *u;
        for (u = t; u; u = u->same) t1 = u;
// t1 is now last real script in the list.
        stringAndString(&cf->dw, &cf->dw_l, "</body>");
        runGeneratedHtml(t, cf->dw);
        run_function_onearg_win(cf, "eb$uplift", t);
        for (u = t1; u; u = u->same) t2 = u;
        if(t1 != t && t2 != t1) {
            Tag *t3 = t->same;
            t->same = t1->same;
            t2->same = t3;
            t1->same = 0;
            for (u = t->same; u != t3; u = u->same)
                if (u->jslink) loadScriptData(u);
        }
    }
    nzFree0(cf->dw);
    cf->dw_l = 0;
}

static void runOnload(void);
static void runConnectedCallback(void);
void runScriptsPending(bool startbrowse)
{
	Tag *t;
	int n;
	bool change, async;
	Frame *f, *save_cf = cf;

// 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;
// completely different behavior before and after browse
// After browse, it clobbers the page.
		if(cf->browseMode && !cf->dw_clobber) {
			debugPrint(3, "document.write clobber 2");
			run_function_onestring_t(cf->bodytag, "eb$dbih",
            strstr(cf->dw, "<body>")+6);
            cf->dw_clobber = true;
        } else {
			stringAndString(&cf->dw, &cf->dw_l, "</body>");
			runGeneratedHtml(cf->bodytag, cf->dw);
		}
		nzFree0(cf->dw);
		cf->dw_l = 0;
	}

top:
	change = false;

	for (t = cw->scriptlist; t; t = t->same) {
//		printf("script %d step %d\n", t->seqno, t->step);
		if (t->dead || !t->jslink || t->step >= 3)
			continue;
// don't execute a script until it is linked into the tree.
		if(!isRooted(t)) continue;
		cf = t->f0;
		loadScriptData(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->jslink || t->step >= 5 || t->step <= 2)
			continue;
// js may have upgraded step
		if((n = get_property_number_t(t, "eb$step")) > 4) {
			t->step = n;
			continue;
		}
		if(!isRooted(t)) continue;
		if(t->async != async) continue;
		cf = t->f0;
		if (!is_subframe(cf, save_cf))
			continue;
		if (intFlag) {
			t->step = 6;
			continue;
		}

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

		if (t->step == 3) {
// waiting for background process to load
			pthread_join(t->loadthread, NULL);
			t->threadjoined = true;
			if (!t->loadsuccess || t->hcode != 200) {
				if (debugLevel >= 3)
					i_printf(MSG_GetJS, t->href, t->hcode);
				t->step = 6;
				continue;
			}
			set_property_string_t(t,
					    (t->inxhr ? "$entire" : "text"),
					    t->value);
			nzFree0(t->value);
			t->step = 4;	// loaded
		}

		runScriptNow(save_cf, t);

		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->jslink && !t->dead &&
		save_cf == t->f0) {
			cf = save_cf;
			run_event_t(t, "onload");
			t->lic = 0;
			change = true;
		}
	}

// If a frame has an onload function, that function might need to run.
// We don't get to wait around for on-demand expansion; we have to expand.
// Fortunately this doesn't happen often.
	for (t = cw->framelist; t; t = t->same) {
		if(!t->f1 && // not expanded yet
		!t->expf && // we haven't tried to expand it yet
		isRooted(t) && // it's in our tree
		typeof_property_t(t, "onload") == EJ_PROP_FUNCTION)
			forceFrameExpand(t);
	}

    if (!async) {
        if(startbrowse) {
            cf = save_cf;
            run_function_bool_win(cf, "readyStateComplete");
            runOnload();
            runConnectedCallback();
            startbrowse = false;
            change = true;
        }
        async = true;
        goto passes;
    }

    if (change) {
    runConnectedCallback();
        goto top;
    }

    cf = save_cf;

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

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

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;
	}
}

/* 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;
}

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);

	debugPrint(3, "parse html from browse");
	const int startpos = htmlScanner(buf, NULL, false);
	nzFree(buf);
	prerender(startpos);

/* 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->jslink && !jsDoorway())
		freeJSContext(cf);

	if (isJSAlive) {
		decorate(startpos, 0);
		set_basehref(cf->hbase);
		if(cf->xmlMode) goto past_html_events;
		loadFinishCSS();
		run_function_bool_win(cf, "eb$qs$start");
		runScriptsPending(true);
		run_event_win(cf, "onfocus");
		run_event_doc(cf, "onfocus");
		runScriptsPending(false);
		rebuildSelectors();
	}
past_html_events:
	debugPrint(3, "end parse html from browse");

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

	return newbuf;
}

/* See if there are simple tags like <p> or </font> */
bool htmlTest(void)
{
	int j, ln;
	char look[12];
	for (ln = 1; ln <= cw->dol; ++ln) {
		char *p = (char *)fetchLine(ln, -1);
// special xml indicator of my own creation
		if(ln == 1 && !memcmp(p, "`~*xml}@;", 9))
			return true;
		while (isspaceByte(*p) && *p != '\n')
			++p;
		if (*p == '\n')
			continue;	// skip blank line
		if (*p != '<') return false;
// 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 (memEqualCI(p + 1, "!if", 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?
                    if(name2tagInfo(look)) return true;
		}
		return false;
	}
	return false;
}

bool browseCurrentBuffer(const char *suffix, bool plain)
{
	char *rawbuf, *newbuf = 0, *tbuf;
	int rawsize, tlen, j;
	bool rc, remote;
	uchar sxfirst = 1;
	bool save_ch = cw->changeMode;
	uchar bmode = 0;
	const struct MIMETYPE *mt = 0;

	remote = isURL(cf->fileName);

	debugPrint(4, "browseCurrent suffix %s plain %d render %d%d%d imap %d remote %d",
	suffix, plain,
	cf->render1, cf->render2, cf->render3,
	(cw->imapMode1 ? 1 : cw->imapMode2 ? 2 : cw->imapMode3 ? 3: 0),
	remote);
	if (!(cf->render2|cf->render3) && (cf->fileName || suffix)) {
		if(suffix) {
// we already checked for valid suffix
			mt = findMimeBySuffix(suffix);
		} else if (remote) {
			mt = findMimeByURL(cf->fileName, &sxfirst);
		} else {
			mt = findMimeByFile(cf->fileName);
		}
	}

// If subject just happens to end in mp3 or such,
// we don't want to run an email through a player.
	if(cw->imapMode3 || (cw->prev && cw->prev->imapMode2)) mt = 0;

	if (mt && !mt->outtype) {
		setError(MSG_NotConverter);
		return false;
	}

	if (mt && mt->from_file && remote) {
		setError(MSG_PluginFile);
		return false;
	}

	if (mt) {
		if (cf->render1 && mt == cf->mt) {
			mt = 0;
			if(suffix) {
				setError(MSG_DoubleBrowse);
				return false;
			}
		} else
			bmode = 3;
	}

	if (!bmode && cw->binMode) {
		setError(MSG_BrowseBinary);
		return false;
	}

	if (bmode) ;		// ok
	else
/* A mail message often contains lots of html tags,
 * so we need to check for email headers first. */
	if (!remote && emailTest())
		bmode = 1;
	else if (htmlTest())
		bmode = 2;
	else {
		setError(MSG_Unbrowsable);
		return false;
	}

	if(bmode != 3 || remote) {
		if (!unfoldBuffer(context, false, &rawbuf, &rawsize))
			return false;	// should never happen
	}

	if (bmode == 3) {
// convert raw text via a plugin
		if(remote) {
			rc = runPluginCommand(mt, cf->fileName, 0, rawbuf,
			      rawsize, &rawbuf, &rawsize);
		} else if(cf->fileName && !access(cf->fileName, 4)) {
// Pass the file directly to the converter for efficiency
			rc = runPluginCommand(mt, 0, cf->fileName, 0, 0,
			      &rawbuf, &rawsize);
		} else {
// no filename or file not present, many ways this could happen.
// You might have changed the filename, whence you should be hit with a rolled
// up newspaper. Or this could be an attachment, prenamed, but not saved
// to your computer yet.
			char standin[20];
			if(!suffix) suffix = file2suffix(cf->fileName);
			if(suffix) sprintf(standin, "standin.%s", suffix);
			else strcpy(standin, "nosuffix");
// we haven't unfolded the buffer yet
			if (!unfoldBuffer(context, false, &rawbuf, &rawsize))
				return false;
			rc = runPluginCommand(mt, 0, standin, rawbuf,
			      rawsize, &rawbuf, &rawsize);
		}
		if (!rc)
			return false;
		if (!cf->render1)
			cf->render1b = true;
		cf->render1 = cf->render2 = true;
		iuReformat(rawbuf, rawsize, &tbuf, &tlen);
		if (tbuf) {
			nzFree(rawbuf);
			rawbuf = tbuf;
			rawsize = tlen;
		}
// make it look like remote html, so we don't get a lot of errors printed
		remote = true;
		if(mt->outtype == 'h') {
			bmode = 2;
			addToFilename(".html");
		} else {
			bmode = 0;
		}
		if(mt->outtype == 't')
			addToFilename(".txt");
		cf->render4 = true;
		if (!allowRedirection)
			bmode = 0;
	}

// this shouldn't do any harm if the output is text
	prepareForBrowse(rawbuf, rawsize);

// No harm in running this code in mail client, but no help either,
// and it begs for bugs, so leave it out.
	if (!ismc) {
		undoCompare();
		cw->undoable = false;
	}

	if (bmode == 1) {
		newbuf = emailParse(rawbuf, plain);
		j = strlen(newbuf);

// mail could need utf8 conversion, after qp decode
		iuReformat(newbuf, j, &tbuf, &tlen);
		if (tbuf) {
			nzFree(newbuf);
			newbuf = tbuf;
			j = tlen;
		}
// pretend like the rendering of mail is done by a plugin
		cf->render1 = cf->render2 = true;

// mail should always be in html, see mailShowsHtml in fetchmail.c
		addToFilename(".html");
		cf->render4 = true;
		if (memEqualCI(newbuf, "<html>\n", 7) && allowRedirection) {
/* double browse, mail then html */
			bmode = 2;
			browseMail = true;
			remote = true;
			rawbuf = newbuf;
			rawsize = j;
			prepareForBrowse(rawbuf, rawsize);
		}
	}

	if (bmode == 2) {
		if (javaOK(cf->fileName))
			createJSContext(cf);
		nzFree0(newlocation);	/* should already be 0 */
		newbuf = htmlParse(rawbuf, remote);
		addToFilename(".browse");
	}

	if (bmode == 0)
		newbuf = rawbuf;

	cw->rnlMode = cw->nlMode;
	cw->nlMode = false;
// I'm gonna assume it ain't binary no more
	cw->binMode = false;
	cw->r_dot = cw->dot, cw->r_dol = cw->dol;
	cw->dot = cw->dol = 0;
	cw->r_map = cw->map;
	cw->map = 0;
	memcpy(cw->r_labels, cw->labels, sizeof(cw->labels));
	memset(cw->labels, 0, sizeof(cw->labels));
	j = strlen(newbuf);
	rc = addTextToBuffer((pst) newbuf, j, 0, false);
	free(newbuf);
	cw->undoable = false;
	cw->changeMode = save_ch;
	if (!rc) {
// should never happen
		fileSize = -1;
		cw->browseMode = cf->browseMode = true;
		return false;
	}

	if (bmode == 2)
		cw->dot = cw->dol;
	cw->browseMode = cf->browseMode = true;
	fileSize = bufferSize(context, true);
	cw->mustrender = false;
	time(&cw->nextrender);
	cw->rr_throttle = 2;
	cw->nextrender += cw->rr_throttle;
	if(cw->prev && cw->prev->imapMode2) cw->imapMode3 = true;
	return true;
}

// Connect an input field to its datalist.
// I use the field ninp for this, rather nonobvious, sorry.
static void connectDatalist(Tag *t)
{
	const char *lista = 0; // list from attributes
	char *listj = 0; // list from javascript
	const Tag *u;
	if(t->action != TAGACT_INPUT || t->itype != INP_TEXT ||
	t->ninp)
		return;
	lista = attribVal(t, "list");
	if(t->jslink && allowJS)
		listj = get_property_string_t(t, "list");
	if(listj && *listj)
		lista = listj;
	if(!lista)
		return;
	if((u = gebi_c(t, lista, false)))
		t->ninp = u->seqno;
	nzFree(listj);
}

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

	connectDatalist(t);

	s = inp_types[t->itype];
	if(t->ninp && t->itype == INP_TEXT)
		s = "suggested select";
	eb_printf("%s", s);
	if (t->multiple)
		eb_printf(" multiple");
	if (t->itype == INP_SELECT) {
		for (v = cw->optlist; v; v = v->same) {
			if (!controlledBy(v, t)) continue;
			if (!v->textval) continue;
			if(inputHidden(v)) break;
		}
		if(v) eb_printf(" with hidden options");
	}
	if (t->itype == INP_TEXT && t->lic)
		eb_printf("[%d]", t->lic);
	if (t->itype_minor != INP_NO_MINOR)
		eb_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) {
			eb_printf("[%s×%s", rows, cols);
			if (wrap && stringEqualCI(wrap, "virtual"))
				i_printf(MSG_Recommended);
			eb_printf("]");
		}
	}			// text area
	if (inputReadonly(t))
		eb_printf(" readonly");
	if (inputDisabled(t))
		eb_printf(" disabled");
	if (t->name)
		eb_printf(" %s", t->name);
	if(t->itype == INP_SUBMIT &&
	!inputDisabled(t) &&
	debugLevel >= 3) {
		const Tag *f = findOpenTag(t, TAGACT_FORM);
		if(f && f->href)
			  eb_printf(" %s", f->href);
		else if(attribVal(t, "onclick"))
			  eb_printf(" %s", attribVal(t, "onclick"));
		}
	nl();

	if(t->ninp && t->itype == INP_TEXT)
		t = tagList[t->ninp];
	else 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 (!controlledBy(v, t)) continue;
		if (!v->textval) continue;
		++cnt;
		if(inputHidden(v)) continue;
		if (*search && !strcasestr(v->textval, search))
			continue;
		if(v->custom_h) {
			eb_printf("    %s", v->custom_h);
			if(v->parent && v->parent->action == TAGACT_OPTG && inputDisabled(v->parent))
				eb_printf("🛑");
			eb_printf("\n");
		}
		show = true;
		eb_printf("%3d %s", cnt, v->textval);
		if(inputDisabled(v)) eb_printf(" 🛑");
		eb_printf("\n");
	}
	if (!show) {
		if (!*search)
			i_puts(MSG_NoOptions);
		else
			i_printf(MSG_NoOptionsMatch, search);
	}
}

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

static bool inputHidden(const Tag *t)
{
	if (allowJS && t->jslink)
		return get_property_bool_t(t, "hidden");
	return t->hidden;
}

static bool inputReadonly(const Tag *t)
{
	if (allowJS && t->jslink)
		return get_property_bool_t(t, "readOnly");
	return t->rdonly;
}
/*********************************************************************
Some image alt texts contain tags, I've only seen one so far.
<a href="/players/c/cabremi01.shtml"><img class="poptip" tip="17. <strong>Miguel&nbsp;Cabrera</strong> (3167)" src="https://www.baseball-reference.com/req/202308280/images/headshots/b/bceca907_br_det.jpg" height="135" width="90" alt="Photo of <strong>Miguel&nbsp;Cabrera</strong>"></a>
I don't think just any tags could be there, like anchors; more like <i> <b> <em>
and other things that edbrowse doesn't process, so I will just remove them.
It is possible that this website simply generates some bad html. I haven't seen it elsewhere.
So this is coded, but commented out.
*********************************************************************/
#if 0
static void stripTag(char *b)
{
	char *s;
	if(!b) return;
	while((b = strchr(b, '<'))) {
		if(isalpha(b[1]) || (b[1] == '/' && isalpha(b[2]))) {
// looks like a tag
			s = strchr(b, '>');
// what to do if there is no closing >
			if(!s) break;
			strcpy(b, s+1);
			continue;
		}
// isolated <
		++b;
	}
}
#endif

static const char *imageAlt(const Tag *t)
{
	char *u, *v;
	if (allowJS && t->jslink)
		u = get_property_string_t(t, "alt");
	else
		u = cloneString(attribVal(t, "alt"));
//	stripTag(u);
	v = altText(u);
	cnzFree(u);
	return v;
}

/*********************************************************************
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 ln1, ln2, n, plen;
	char *p1, *p2, *s, *t, *new;

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

	if (fromForm)
		i_printf(MSG_NoTagFound, tagno, newtext);
}

/* Count the number of UTF-8 characters in a string. */
static size_t utf8length(const char *s)
{
	size_t len = 0;
		while (*s) {
			if ((*s & '\x7F') == *s || (*s & '\xBF') != *s) len++;
			++s;
		}
	return len;
}

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

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

	prepareForField(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) {
		if(t->lic > 0) cxQuit(t->lic, 3);
		t->lic = -1;
	}

	if (inputReadonly(t)) {
		setError(MSG_Readonly);
		return false;
	}
	if (inputDisabled(t)) {
		setError(MSG_Disabled);
		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;
		}
	}

// It looks like the input field change is going to work.
// Clear the sank bit so we sync up this change with javascript before
// it runs again.
	jClearSync();

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

	if (itype == INP_FILE) {
		int u_l;
		char *u = initString(&u_l);
		if(!newtext[0]) { // empty
			updateFieldInBuffer(tagno, newtext, notify, true);
			return true;
		}
		if(!t->multiple) {
			const char *z2;
			if (!envFile(newtext, &z2))
				return false;
			if (z2[0] && (access(z2, 4) || fileTypeByName(z2, 0) != 'f')) {
				setError(MSG_FileAccess, z2);
				return false;
			}
			u = cloneString(z2);
		} else {
			const char *v = newtext, *w, *z2;
			char *z;
			while(*v) {
				if(!(w = strchr(v, selsep)))
					w = v + strlen(v);
				z = pullString(v, w-v);
				v = *w ? w+1 : w; // point to next file
				if(!*z) { // empty
					nzFree(z);
					continue;
				}
				if (!envFile(z, &z2)) {
					nzFree(z), nzFree(u);
					return false;
				}
				if (z2[0] && (access(z2, 4) || fileTypeByName(z2, 0) != 'f')) {
					setError(MSG_FileAccess, z2);
					nzFree(z), nzFree(u);
					return false;
				}
				if(*u)
					stringAndChar(&u, &u_l, selsep);
				stringAndString(&u, &u_l, z2);
				nzFree(z);
			}
		}
		updateFieldInBuffer(tagno, u, notify, true);
		nzFree(u);
		return true;
	}

	if(itype == INP_TEXT) {
		connectDatalist(t);
		if (t->ninp) { // the suggested select
// this is a strange puppy.
// ` to override
			if(newtext[0] == '`') {
				++newtext, --newlen;
			} else {
				const Tag *options = tagList[t->ninp];
				if (!locateOptions(options, newtext, 0, 0, false))
					return false;
				locateOptions(options, newtext, &display, 0, false);
				newtext = display;
				newlen = strlen(newtext);
			}
		}

		if (t->lic && (int)utf8length(newtext) > t->lic) {
			setError(MSG_InputLong, t->lic);
			goto fail;
		}

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

		if (itype_minor == INP_EMAIL && *newtext && ((!t->multiple && !isEmailAddress(newtext)) || (t->multiple && !isEmailAddressList(newtext)))) {
			setError(MSG_EmailInput);
			goto fail;
		}

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

	if (itype == INP_RADIO && form && t->name && *newtext == '+') {
/* clear the other radio button */
		for (v = cw->inputlist; v; v = v->same) {
			if (!controlledBy(v, 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, t);
		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(t, "onclick");
			if (js_redirects)
				return true;
		}
		if (itype != INP_SELECT)
			bubble_event_t(t, "oninput");
		if (js_redirects)
			goto success;
		bubble_event_t(t, "onchange");
		if (js_redirects)
			goto success;
		jSideEffects();
	}

success:
	nzFree(display);
	return true;

fail:
	nzFree(display);
	return false;
}

// return an allocated string containing the text entries for the checked options
char *displayOptions(const Tag *sel)
{
	const Tag *t;
	char *opt;
	int opt_l;

	opt = initString(&opt_l);
	for (t = cw->optlist; t; t = t->same) {
		if (!controlledBy(t, sel)) continue;
		if (!t->checked) continue;
		if (*opt)
			stringAndChar(&opt, &opt_l, selsep);
		stringAndString(&opt, &opt_l, t->textval);
	}

	return opt;
}

/*********************************************************************
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;
	const char *w = t->rvalue;
	bool bval = false;

/* 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->jslink || !allowJS)
		return;

	if (itype >= INP_RADIO) {
		set_property_bool_t(t, "checked", bval);
	} else if (itype == INP_SELECT) {
/* remember this means option */
		set_property_bool_t(t, "selected", bval);
		const Tag *sel = findOpenTag(t, TAGACT_INPUT);
		if(bval && sel && sel->itype == INP_SELECT &&
		!sel->multiple && sel->jslink)
			set_property_number_t(sel,
					    "selectedIndex", t->lic);
	} else
		set_property_string_t(t, "value", w);
}

static void formReset(const Tag *form)
{
	Tag *t;
	int i, itype;
	char *display;

	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 (!controlledBy(t, form)) continue;
		itype = t->itype;
		if (itype != INP_SELECT) {
			resetVar(t);
			continue;
		}
		if (t->jslink && allowJS)
			set_property_number_t(t, "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 (!controlledBy(t, 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 */

	if (debugLevel >= 1)
		i_puts(MSG_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->jslink && allowJS)
			return get_property_string_t(t, "value");
	}

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

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

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

	if (t->jslink && allowJS)
		return get_property_bool_t(t,
					 (t->action == TAGACT_OPTION ? "selected" : "checked"));

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

/* 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);
}

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

	if (!val)
		val = emptyString;

	if(fsep && fsep != 'g')
		postDelimiter(fsep);

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

	case 0:
		stringAndString(&pfs, &pfs_l, name);
		stringAndChar(&pfs, &pfs_l, '=');
		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': case 0:
		stringAndString(&pfs, &pfs_l, val);
		stringAndString(&pfs, &pfs_l, eol);
		break;

	case 'g':
		if (pfs[pfs_l - 1] == '?') {
			pfs[pfs_l - 1] = '\t';
			stringAndString(&pfs, &pfs_l, val);
		}
		break;

	case '-':
		if (isfile) {
			if (isfile & 2) {
				stringAndString(&pfs, &pfs_l, "; filename=\"");
// only show the filename
				cut = strrchr(val, '/');
				stringAndString(&pfs, &pfs_l, (cut ? cut + 1 : val));
				stringAndChar(&pfs, &pfs_l, '"');
			}
			if (!encodeAttachment(val, 0, true, &ct, &ce, &enc, 0))
				return false;
			if(!(isfile&2))
				enc = makeDosNewlines(enc);
/* remember to free enc in this case */
			val = enc;
		} else {
			const char *s;
			ct = "text/plain";
/* Anything nonascii makes it 8bit */
			ce = "7bit";
			for (s = val; *s; ++s)
				if (*s & 0x80) {
					ce = "8bit";
					break;
				}
		}
		if(!stringEqual(ct, "text/plain")) {
			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;
}

static bool formSubmit(const Tag *form, const Tag *submit, bool dopost, const char *const prot)
{
	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 requireName = true; // only include form controls with a non-blank name attribute
	bool rc;
	bool bval;
	const char *eo1; // enctype override from attribute
	char *eo2; // enctype override from js

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

	if (form->bymail)
		fsep = '\n';

// if method is not post, these other encodings are not honored
	if(!dopost)
		goto skip_encode;

	if (form->mime)
		fsep = '-';
	if(form->plain)
		fsep = 0;

// <input enctype=blah> can override
	if(submit) {
		eo1 = attribVal(submit, "formenctype"), eo2 = 0;
		if(submit->jslink && allowJS)
			eo2 = get_property_string_t(submit, "formenctype");
		if(eo2 && *eo2)
			eo1 = eo2;
		if(eo1 && *eo1) {
			fsep = '&';
			if (stringEqualCI(eo1, "multipart/form-data"))
				fsep = '-';
			else if (stringEqualCI(eo1, "text/plain"))
				fsep = 0;
			else if (!stringEqualCI(eo1, 
				  "application/x-www-form-urlencoded"))
				debugPrint(3,
					   "unrecognized enctype, plese use multipart/form-data or application/x-www-form-urlencoded or text/plain");
		}
		nzFree(eo2);
	}

	if (fsep == '-') {
		boundary = makeBoundary();
		stringAndString(&pfs, &pfs_l, "`mfd~");
		stringAndString(&pfs, &pfs_l, boundary);
		stringAndString(&pfs, &pfs_l, eol);
	}

skip_encode:
	if (stringEqualCI(prot, "gopher") || stringEqualCI(prot, "gophers")) {
		requireName = false;
		fsep = 'g';
	}

	for (t = cw->inputlist; t; t = t->same) {
		if (!controlledBy(t, form)) continue;
		itype = t->itype;
		if (itype <= INP_SUBMIT && t != submit) continue;
		if (inputDisabled(t)) continue;
		name = t->name;
		if (requireName && (!name || !*name)) continue;
		if (!name) name = emptyString;

		if (t == submit) {	/* the submit button you pushed */
			int namelen;
			char *nx;
			value = t->value;
			if (!value)
				value = "Submit";
			else if (!*value && stringEqual(t->info->name, "input"))
				if (stringInListCI(t->attributes, "value") < 0)
					value = "Submit";

			if (t->itype != INP_IMAGE)
				goto success;
			namelen = strlen(name);
			createFormattedString(&nx, "%s.x", name);
			postNameVal(nx, "0", fsep, false);
			nx[namelen + 1] = 'y';
			postNameVal(nx, "0", fsep, false);
			nzFree(nx);
			goto success;
		}

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

// special case for charset
		if(itype == INP_HIDDEN && stringEqualCI(name, "_charset_")) {
			value = (cf->charset ? cf->charset : "UTF-8");
			eo1 = attribVal(form, "accept-charset"), eo2 = 0;
			if(form->jslink && allowJS)
				eo2 = get_property_string_t(form, "accept-charset");
			if(eo2 && *eo2)
				eo1 = eo2;
			if(eo1 && *eo1)
				value = eo1;
			postNameVal(name, value, fsep, false);
			nzFree(eo2);
			continue;
		}

		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);
			if(t->required && !dynamicvalue && !*dynamicvalue)
				goto required;
			nzFree0(dynamicvalue);
			continue;
		}

		if (itype == INP_TA) {
			int cx = t->lic;
			char *cxbuf;
			int cxlen;
			if(cx < 0) {
				dynamicvalue = fetchTextVar(t);
				if(!dynamicvalue) // don't know what happened
				cx = 0;
			}
			if (cx) {
				if (fsep == '-') {
// do this as an attachment
					char cxstring[12];
					if(cx < 0) {
						cx = sideBuffer(0, dynamicvalue, -1, NULL);
						nzFree0(dynamicvalue);
					}
					sprintf(cxstring, "%d", cx);
					rc = postNameVal
					    (name, cxstring, fsep, 1);
					if(t->lic < 0) cxQuit(cx, 3);
					if(!rc)
						goto fail;
					continue;
				} // attach
				if(cx < 0)
					cxbuf = dynamicvalue, cxlen = strlen(dynamicvalue);
				else 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)
					goto fail;
				if(t->required && !j)
					goto required;
				continue;
			}

			postNameVal(name, 0, fsep, false);
			if(t->required)
				goto required;
			continue;
		}

/*********************************************************************
Here is a small page to test some of these select option cases.
<body><form action=http://www.eklhad.net/cgi-bin/testperl>
<select name=snork multiple>
<option value=y>Yes</option>
<option value="" selected>No</option>
<hr>
<option>Maybe</option>
<input type=submit>
</select></form></body>
*********************************************************************/

		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 (controlledBy(v, t))
						v->checked = v->rchecked;
				display = displayOptions(t);
			}
			rc = locateOptions(t, display, 0, &dynamicvalue, false);
//			printf("disp<%s>dyn<%s>\n", display, dynamicvalue);
			if (!rc) { // this should never happen
				nzFree(display);
				goto fail;
			}
// unlike display, value can return null, if no choice was made
			if (!dynamicvalue) {
				nzFree(display);
				if(t->required) goto required;
				continue;
			}

// Single select cannot select a blank value
// if the option selected is first in list. Wow.
			if(!t->required || t->multiple || *dynamicvalue)
				goto options_ok;
			const char *z; int zv;
// setting size to something > 1 disables this behavior.
			if((z = attribVal(t, "size")) && (zv = stringIsNum(z)) && zv > 1)
				goto options_ok;
			const Tag *u = locateOptionByNum(t, 1);
			if(u && stringEqual(u->textval, display) && !*u->value &&
			!(u->parent && u->parent->action == TAGACT_OPTG)) {
				nzFree(display);
				goto required;
			}
options_ok:
			nzFree(display);

// Now step through the options
			char more = 1;
			for (s = dynamicvalue; more; s = e) {
				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;
			}
			nzFree0(dynamicvalue);
			continue;
		}

		if (itype == INP_FILE) {	/* the only one left */
			uchar isfile = 3;
			dynamicvalue = fetchTextVar(t);
			if (!dynamicvalue || !*dynamicvalue) {
				if(t->required)
					goto required;
				postNameVal(name, emptyString, fsep, 0);
				continue;
			}
			if (!dopost  || fsep != '-') {
				if(fsep == '\n') {
					setError(MSG_FilePost);
					nzFree(dynamicvalue);
					goto fail;
				}
// we'll try to truck along
				isfile = 0;
				if(debugLevel >= 3)
					i_puts(MSG_FilePost);
			}

			if(!t->multiple) {
				rc = postNameVal(name, dynamicvalue, fsep, isfile);
				nzFree0(dynamicvalue);
				if (!rc)
					goto fail;
			} else {
				const char *v = dynamicvalue, *w;
				char *z;

				while(*v) {
					if(!(w = strchr(v, selsep)))
						w = v + strlen(v);
					z = pullString(v, w-v);
					v = *w ? w+1 : w; // point to next file
					if(!*z) { // empty
						nzFree(z);
						continue;
					}
					rc = postNameVal(name, z, fsep, isfile);
					nzFree(z);
					if (!rc)
						goto fail;
				}

				nzFree(dynamicvalue);
			}
			continue;
		}

		i_printfExit(MSG_UnexSubmitForm);

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

	if (fsep == '-') {	// the last boundary
		stringAndString(&pfs, &pfs_l, "--");
		stringAndString(&pfs, &pfs_l, boundary);
		stringAndString(&pfs, &pfs_l, "--\r\n");
	}

	return true;

fail:
	return false;

required:
	setError(MSG_ReqField, name);
	return false;
}

/*********************************************************************
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.
If post_string is NULL, print the action and do not submit the form or trigger JavaScript events.
*********************************************************************/

/* 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 *action2 = 0; // allocated action
	char *section;
	const char *prot;
	bool rc, dopost;

	if (post_string)
		*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 = findOpenTag(t, TAGACT_FORM);
		itype = t->itype;
	}

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

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

	if (post_string)
		cw->nextrender = 0;

	if (itype == INP_BUTTON) {
		if (post_string) {
/* 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->jslink && !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 (post_string) {
			if (t && tagHandler(form->seqno, "onreset")) {
				if (!allowJS)
					runningError(MSG_NJNoReset);
				else {
					rc = true;
					if (form->jslink)
						rc = run_event_t(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 (post_string) {
		if (t && tagHandler(form->seqno, "onsubmit")) {
			if (!allowJS)
				runningError(MSG_NJNoSubmit);
			else {
				rc = true;
				if (form->jslink)
					rc = bubble_event_t(form, "onsubmit");
				if (!rc) return true;
				if (js_redirects)
					return true;
			}
		}
	}

	dopost = form->post;
	action = form->href;
	if (!action && form->jslink && allowJS) {
		char *jh = get_property_url_t(form, true);
		if (jh && (!action || !stringEqual(jh, action))) {
			nzFree(form->href);
			action = form->href = jh;
			jh = NULL;
		}
		nzFree(jh);
	}

	if(t) { // submit button pressed
		const char *va; // value from attribute
		char *vj; // value from javascript
// spec says formAction and formMethod are camelcase, when coming from js.
// Are they lowercase when coming from html? or case insensitive?
		va = attribVal(t, "formmethod");
		vj = 0;
		if(t->jslink && allowJS)
			vj = get_property_string_t(t, "formMethod");
		if(vj && *vj)
			va = vj;
		if(va && *va) {
			dopost = false;
			if (stringEqualCI(va, "post"))
				dopost = true;
			else if (!stringEqualCI(va, "get"))
				debugPrint(3, "unrecognized method, please use get or post");
		}
		nzFree(vj);
		va = attribVal(t, "formaction");
		if(t->jslink && allowJS)
			action2 = vj = get_property_string_t(t, "formAction");
		if(vj && *vj)
			va = vj;
		if(va && *va)
			action = va;
	}

// 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))
			goto success;
		action = f->hbase;
	}

	prot = getProtURL(action);
	if (!prot) {
		if (t && t->onclick)
			goto success;
		setError(MSG_FormBadURL);
fail:
		nzFree(action2);
		return false;
	}

	if (debugLevel >= 2 || !post_string)
		eb_printf("%c %s\n", (post_string ? '*' : ' '), action);

	if (stringEqualCI(prot, "javascript")) {
		if (post_string) {
			if (!allowJS) {
				setError(MSG_NJNoForm);
				goto fail;
			}
			jsRunScript_t(form, action, 0, 0);
		}
		goto success;
	}

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

	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 | dopost))) {
		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, (dopost ? '\1' : '?'));
	actlen = strlen(pfs);

	if (formSubmit(form, t, dopost,    prot)) {
		if(post_string && debugLevel >= 1)
			i_puts(MSG_FormSubmit);
	} else {
		nzFree(pfs);
		goto fail;
	}

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

/* Handle the mail method here and now. */
	if (form->bymail) {
		char *addr, *subj, *q;
		const char *tolist[MAXCC + 2], *atlist[MAXCC + 2];
		const char *name = form->name;
		int newlen = strlen(pfs) - actlen;	/* the new string could be longer than post */
		char key;
		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);
		printf("sending mail to %s", addr);
		if(subj)
			printf(", subject %s", subj);
		printf(", is this ok? ");
		fflush(stdout);
		key = getLetter("ynYN");
		puts("");
		if(key == 'y' || key == 'Y') {
			rc = sendMail(localAccount, tolist, q, -1, atlist, 0, 0, 0, false);
			if (rc)
				i_puts(MSG_MailSent);
		} else
			rc = true;
		nzFree(addr);
		nzFree(subj);
		nzFree(q);
		nzFree(action2);
		return rc;
	}

	if (post_string)
		*post_string = pfs;
	else
		nzFree(pfs);
success:
	nzFree(action2);
	return true;
}

void domSubmitsForm(Tag *t, bool reset)
{
	if (reset)
		js_reset = t;
	else
		js_submit = t;
}

void domSetsTagValue(Tag *t, const char *newtext)
{
	if (t->itype == INP_HIDDEN || t->itype == INP_RADIO
	    || t->itype == INP_FILE)
		return;
	nzFree(t->value);
	t->value = cloneString(newtext);
	if (t->itype == INP_TA) {
		int side = t->lic;
		if(side < 0) side = t->lic = 0;
		if (side == 0 || side > maxSession || side == context)
			return;
		if (sessionList[side].lw == NULL)
			return;
		if (cw->browseMode)
			i_printf(MSG_BufferUpdated, side);
		sideBuffer(side, newtext, -1, 0);
		return;
	}
}

bool charInOptions(char c)
{
	const Window *w;
	Tag *t;
	int i;
	Tag *sel;
	for(i = 1; i <= maxSession; ++i) {
		for(w = sessionList[i].lw; w; w = w->prev) {
			if(!w->browseMode) continue;
			for (t = w->optlist; t; t = t->same)
				if(t->textval && strchr(t->textval, c) &&
				(sel = findOpenTag(t, TAGACT_INPUT)) &&
				sel->itype == INP_SELECT && sel->multiple)
					return true;
		}
	}
	return false;
}

void charFixOptions(char c)
{
	Window *w, *save_w = cw;
	Tag *t;
	int i, j;
	char *u;
	for(i = 1; i <= maxSession; ++i) {
		for(w = sessionList[i].lw; w; w = w->prev) {
			if(!w->browseMode) continue;
			for(j = 0; j < w->numTags; ++j) {
				t = w->tags[j];
				if(t->action != TAGACT_INPUT || !t->value ||
				t->itype != INP_SELECT || !t->multiple)
					continue;
				for(u = t->value; *u; ++u)
					if(*u == c)
						*u = selsep;
			}
			cw = w, rerender(cw == save_w ? 0 : -1);
		}
	}
	cw = save_w;
}

bool charInFiles(char c)
{
	const Window *w;
	const Tag *t;
	int i, j;
	char *u;
	for(i = 1; i <= maxSession; ++i) {
		for(w = sessionList[i].lw; w; w = w->prev) {
			if(!w->browseMode)
				continue;
			for(j = 0; j < w->numTags; ++j) {
				t = w->tags[j];
				if(t->action != TAGACT_INPUT ||
				t->itype != INP_FILE || !t->multiple ||
				!(u = fetchTextVar(t)))
					continue;
				if(strchr(u, c)) {
					nzFree(u);
					return true;
				}
				nzFree(u);
			}
		}
	}
	return false;
}

void charFixFiles(char c)
{
	Window *w, *save_w = cw;
	Tag *t;
	int i, j;
	char *u, *v;
	for(i = 1; i <= maxSession; ++i) {
		for(w = sessionList[i].lw; w; w = w->prev) {
			if(!w->browseMode) continue;
			cw = w;
			for(j = 0; j < w->numTags; ++j) {
				t = w->tags[j];
				if(t->action != TAGACT_INPUT ||
				t->itype != INP_FILE || !t->multiple ||
				!(u = fetchTextVar(t)))
					continue;
				for(v = u; *v; ++v)
					if(*v == c)
						*v = selsep;
				updateFieldInBuffer(t->seqno, u, false, false);
				nzFree(t->value);
				t->value = u;
			}
		}
	}
	cw = save_w;
}

// getElementById in C.
// Don't scan through all tags; we have to stay within our frame.
static bool anchorlook;
static const char *idsearch;
static const Tag *gebi_r(const Tag *t); // recursive
const Tag *gebi_c(const Tag *t, const char *id, bool lookname)
{
	int action;
	if(!id || !*id)
		return 0;
// this should be easy, but bugs keep popping up.
	debugPrint(4, "search %s", id);
	while (true) {
		action = t->action;
		debugPrint(4, "up %d,%s", t->seqno, t->info->name);
// don't go past document and up to a higher frame
		if(action == TAGACT_HTML || action == TAGACT_FRAME)
			return 0;
		if(action == TAGACT_BODY)
			break;
		if(!(t = t->parent))
			return 0; // should never happen
	}
// t is <body> at the top of the current frame
	idsearch = id;
	anchorlook = lookname;
	return gebi_r(t);
}

static const Tag *gebi_r(const Tag *t)
{
	const Tag *c; // children
	const Tag *u;
	debugPrint(4, "look %d,%s,%s", t->seqno, t->info->name, t->id);
	if(t->id && stringEqual(t->id, idsearch))
		return t;
	if (anchorlook && t->action == TAGACT_A &&
	t->name && stringEqual(t->name, idsearch))
		return t;
// do not descend into a new frame
	if(t->action == TAGACT_FRAME)
		return 0;
// look through children
	for (c = t->firstchild; c; c = c->sibling)
		if((u = gebi_r(c)))
			return u;
	return 0;
}

/* 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_message(msg), p);
	va_end(p);
	nl();
}

/*********************************************************************
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 && !*s2) {
			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;
}

// 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;

// Everything past this point just prints update messages.
// So return true if we printed stuff.

	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 if(sameFront + 1 == cw->dot)
				printDot();
			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) {
			if(cw->dot == sameFront + 1) {
				i_printf(MSG_LineUpdate1, sameBack2);
				printDot();
			} else if(cw->dot == sameBack2) {
				i_printf(MSG_LineUpdate1, sameFront + 1);
				printDot();
			} else i_printf(MSG_LineUpdateZ1, sameFront + 1, sameBack2);
		} else {
			if(cw->dot == sameFront + 1) {
				i_printf(MSG_LineUpdate3, front2z + 1,  sameBack2);
				printDot();
			} 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 if(sameBack2 == cw->dot)
			printDot();
		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 {
		if(cw->dot == sameBack2) {
			i_printf(MSG_LineUpdate3, sameFront + 1, back2z - 1);
			printDot();
		} 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;
}

static void silent(int msg, ...)
{
    (void) 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_AREA ||
	((action == TAGACT_SPAN || action == TAGACT_DIV) && 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, rrcount;

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

	if(!cw->browseMode) return;
	debugPrint(4, "rerender(%d)", rr_command);
// it's possible to be in browse mode with no tags to traverse.
// Example, browse an email file with hr-
// Then rerender clears the page, which is bad!
	if(!cw->tags) {
		if (rr_command > 0)
			i_puts(MSG_NoChange);
		return;
	}

	cw->mustrender = false;
	time(&cw->nextrender);
	if(cw->rr_throttle == 0) cw->rr_throttle = 1;
	if(cw->rr_throttle < rr_interval) {
		cw->rr_throttle *= 3;
		cw->rr_throttle += 1;
		cw->rr_throttle /= 2;
	}
	if(cw->rr_throttle > rr_interval) cw->rr_throttle = rr_interval;
	cw->nextrender += cw->rr_throttle;
	hovcount = invcount = injcount = rrcount = 0;

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

	if (rr_command > 0) {
// You might have changed some input fields on the screen, then typed rr
		jSyncup(true, 0);
	}
// 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();
	newbuf = htmlReformat(a);
	nzFree(a);
	debugPrint(4, "%d nodes rendered", rrcount);

	if (rr_command > 0 && 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 > 0)
			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.
*********************************************************************/

	if(rr_command < 0)
		goto done;

	removeHiddenNumbers((pst) snap, 0, context, 0);
	removeHiddenNumbers((pst) newbuf, 0, context, 0);
	if (stringEqual(snap, newbuf)) {
		if (rr_command > 0)
			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();

// Update from javascript means the lines move, and our undo is unreliable.
// Here is a complicated if, cause often the current line is unaffected.
	if(undo1line <= sameFront || // before any changes
	(sameBack1 == sameBack2 && (
	undo1line > sameBack1 || (
	front1z == front2z && back1z == back2z && (
	(back1z > 0 && undo1line >= back1z && undo1line < sameBack1) ||
	(front1z > 0 && undo1line <= front1z && undo1line > sameFront + 1))))))
		;
	else
		undoSpecialClear();

// 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) {
			if(sameFront + 1 == cw->dot && !z)
				printDot();
			else
				(*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);
}

/* 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;
		}
	}
}

/* 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>");
}

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

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

run_event_doc(cf, "onDOMContentLoaded");

/* window and document onload */
	run_event_win(cf, "onload");
	if (intFlag)
		return;
	run_event_doc(cf, "onload");
	if (intFlag)
		return;

	fn = -1;
	for (i = 0; i < cw->numTags; ++i) {
		if (intFlag)
			return;
		t = tagList[i];
		if (t->f0 != cf)
			continue;
		if(!isRooted(t)) continue;
		action = t->action;
		if (action == TAGACT_FORM)
			++fn;
		if (!t->jslink)
			continue;
		if (action == TAGACT_BODY)
			run_event_t(t, "onload");
		if (action == TAGACT_BODY && t->onunload)
			unloadHyperlink("document.body.onunload", "Body");
		if (action == TAGACT_FORM)
			run_event_t(t, "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)
			run_event_t(t, "onload");
	}
}

static void runConnectedCallback(void)
{
    Frame *save_cf = cf;
    for(cf = &cw->f0; cf; cf = cf->next)
        run_function_bool_win(cf, "connectedCallbackStart");
    cf = save_cf;
}

// In one place, tack on the $$Aarray to turn onfoo into onfoo$$array
const char *tack_array(const char *e)
{
	static char buf[64];
	int l = strlen(e);
	if((unsigned)l + 7 >= sizeof(buf)) {
		debugPrint(3, "%s$$fn too long", e);
		return 0;
	}
// ontimer is our simulated event handler.
	if(stringEqual(e, "ontimer"))
		return 0;
	sprintf(buf, "%s$$array", e);
	return buf;
}

/*********************************************************************
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,
and an interval flag to repeat.
The usual pathway is setTimeout(), whence backlink is the name
of the timer object under window.
Timer object.backlink also holds the name, so we don't forget it.
jt->t will be 0. There is no tag with this timer.
jt->tsn is a timer sequence number, globally, to help us keep track.
Another path is an asynchronous script.
If we have browsed the page, and down_jsbg is true,
downloading js in background,
then runScriptsPending doesn't run the script, it callse scriptOnTimer(),
and thereby puts the script on a timer.
The timer runs as an interval, according to asyncTimer ms.
The script is out of the hands of runScriptsPending,
and eventually executed by runTimer.
The object on the tag is the script object.
There is yet another path, asynchronous xhr.
Like the above, the page must be browsed, and down_jsbg true.
A tag is created, of type Object, not Script.
The tag is connected to the XHR object, not a Script object.
This is given a backlink name from window, with o.backlink having the same name.
This is the same procedure as the timer objects.
The links protect these objects from garbage collection,
but we have to remember to unlink them.
*********************************************************************/

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

/* 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
200 ms. This really should be a configurable limit.
Timers often run faster than that but edbrowse can't keep up.
We only rerender the screen every 20 seconds or so anyways.
*********************************************************************/
static const int timerSpread = 100;
int timer_sn;			// timer sequence number

void domSetsTimeout(int n, const char *jsrc, const char *backlink, bool isInterval)
{
	struct jsTimer *jt;
	int seqno;
	int n2;

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

	if (stringEqual(jsrc, "-")) {
// Delete a timer. Comes from clearTimeout(obj).
		seqno = n;
		foreach(jt, timerList) {
			if (jt->tsn != seqno)
				continue;
			debugPrint(3, "timer %d delete from context %d", seqno,
			jt->f ? jt->f->gsn: -1);
// a running timer will often delete itself.
			if (jt->running) {
				jt->deleted = true;
			} else {
				if (backlink)
					delete_property_win(jt->f, backlink);
				delFromList(jt);
				nzFree(jt->backlink);
				nzFree(jt);
			}
			return;
		}
// not found, just return.
		return;
	}

// now adding a timer
	jt = allocZeroMem(sizeof(struct jsTimer));
	if(stringEqual(jsrc, "@@pending"))
		jt->pending = true;
	else {
		if (n < timerSpread)
			n = timerSpread;
	}
	if ((jt->isInterval = isInterval))
		jt->jump_sec = n / 1000, jt->jump_ms = n % 1000;
	n2 = n;
// promise jobs not throttled by timerspeed
	if(timerspeed > 1 && !jt->pending &&
		0x40000000 / timerspeed >= n)
		n2 *= timerspeed;
	jt->sec = n2 / 1000;
	jt->ms = n2 % 1000;
	currentTime();
	jt->sec += now_sec;
	jt->ms += now_ms;
	if (jt->ms >= 1000)
		jt->ms -= 1000, ++jt->sec;
	jt->backlink = cloneString(backlink);
	jt->f = cf;
	addToListBack(&timerList, jt);
	seqno = timer_sn;
	debugPrint(3, "timer %d add to context %d under %s",
	seqno, (cf ? cf->gsn : -1), backlink);
	jt->tsn = seqno;
}

void scriptOnTimer(Tag *t)
{
	struct jsTimer *jt = allocZeroMem(sizeof(struct jsTimer));
// asychronous scripts or xhr are not throttled by timerspeed
	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->f = cf;
	addToListBack(&timerList, jt);
	debugPrint(3, "timer %s%d=%s context %d",
		   (t->action == TAGACT_SCRIPT ? "script" : "xhr"),
		   ++timer_sn, t->href, cf->gsn);
	jt->tsn = timer_sn;
}

static struct jsTimer *soonest(void)
{
	struct jsTimer *t, *best_t = 0;
	const Window *w;
	if (listIsEmpty(&timerList))
		return 0;
	foreach(t, timerList) {
		if(!t->pending) {
// regular timer, not the pending jobs timer
			if(!allowJS || !gotimers)
				continue;
// Browsing a new web page in the current session pushes the old one, like ^z
// in Linux. The prior page suspends, and the timers suspend.
// ^ is like fg, bringing it back to life.
			w = t->f->owner;
			if(sessionList[w->sno].lw != w)
				continue;
		}
		if (!best_t || t->sec < best_t->sec ||
		    (t->sec == best_t->sec && t->ms < best_t->ms))
			best_t = t;
	}
	return best_t;
}

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

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

	if (!(jt = soonest())) {
		if (!allowJS || !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 (allowJS && cw->mustrender && remaining <= *delay_sec) {
		*delay_sec = remaining;
		*delay_ms = 0;
	}

	return true;
}

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

static void runTimer0(struct jsTimer *jt, const Frame *save_cf);
// Since timer could be in another frame or even another window,
// I save then restore cw and cf
void runTimer(void)
{
	struct jsTimer *jt;
	Window *save_cw = cw;
	Frame *save_cf = cf;

	currentTime();

/*********************************************************************
I should mention that the following rarely happens.
We have a virtual timer that is always looking for pending jobs, messages, etc.
This is a fast interval, several tims per second.
So ssoonest always returns something, and it is almost always ready to run.
*********************************************************************/
	if (!(jt = soonest()) ||
	    (jt->sec > now_sec || (jt->sec == now_sec && jt->ms > now_ms)))
		return;

	ircRead();

/*********************************************************************
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 corner case.
*********************************************************************/
	if (foregroundWindow)
		jSyncup(true, 0);

	if(jt->pending) { // pending jobs
		if(allowJS) {
/*********************************************************************
pending jobs, microtasks in particular, are suppose to run before timers.
Sometimes there are dozens of these.
This logic doesn't guarantee it but is moving toward it.
If we've done some pending jobs, there might be more, so goto done and don't do
any of the timers.
Loop back around, see if the user has typed a key, if not come right back
on the same timer and do some more.
*********************************************************************/
			if(my_ExecutePendingJobs()) goto done;
			if(my_ExecutePendingMessages()
                        || my_ExecutePendingMessagePorts()) goto done;
		}
// promise jobs not throttled by timerspeed
		int n = jt->jump_sec * 1000 + jt->jump_ms;
		jt->sec = now_sec + n / 1000;
		jt->ms = now_ms + n % 1000;
		if (jt->ms >= 1000)
			jt->ms -= 1000, ++jt->sec;
		cw = save_cw, cf = save_cf;
		return;
	}

	runTimer0(jt, save_cf);

done:
	cw = save_cw, cf = save_cf;
	jSideEffects();
}

static void runTimer0(struct jsTimer *jt, const Frame *save_cf)
{
	Tag *t;

	if (!gotimers) goto skip_execution;

	cf = jt->f;
	cw = cf->owner;
	jt->running = true;
	if ((t = jt->t)) {
// asynchronous script or xhr
		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
				t->threadjoined = true;
				if (!t->loadsuccess ||
				(t->action == TAGACT_SCRIPT &&  t->hcode != 200)) {
					if (debugLevel >= 3)
						i_printf(MSG_GetJS,
							 t->href, t->hcode);
					t->step = 6;
				} else {
					if (t->action == TAGACT_SCRIPT) {
						set_property_string_t(t, "text", t->value);
						nzFree0(t->value);
					}
					t->step = 4;	// loaded
				}
			}
		}
		if (t->step == 4 && t->action == TAGACT_SCRIPT) {
			char *sourcefile = t->js_file;
			int ln = t->js_ln;
                        const char *a = get_property_string_t(t, "type");
			t->step = 5;	// running
			if (!sourcefile) sourcefile = "generated";
			if (!ln) ln = 1;
			if (ln > 1) ++ln;
			if (cf != save_cf)
				debugPrint(4,
					   "running script at a lower frame %s",
					   sourcefile);
			debugPrint(3, "async exec timer %d %s at %d",
				   jt->tsn, sourcefile, ln);
			jsRunData(t, sourcefile, ln,
                                (a && *a && stringEqualCI(a, "module")));
                        cnzFree(a);
			debugPrint(3, "async exec complete");
		}
		if (t->step == 4 && t->action != TAGACT_SCRIPT) {
			t->step = 5;
			set_property_string_t(t, "$entire", t->value);
			nzFree0(t->value);
			debugPrint(3, "run xhr %d", jt->tsn);
			run_function_bool_t(t, "parseResponse");
		}
		if (t->step >= 5)
			jt->deleted = true;
	} else {
// regular timer
		debugPrint(4, "exec timer %d context %d", jt->tsn, jt->f->gsn);
		run_ontimer(jt->f, jt->backlink);
		debugPrint(4, "exec complete");
	}
	jt->running = false;
skip_execution:

	if (!jt->isInterval || jt->deleted) {
		debugPrint(3, "timer %d complete in context %d under %s",
		jt->tsn, (jt->f ? jt->f->gsn : -1), jt->backlink);
// at debug 3 or higher, keep these around, in case you have to
// track down an error.
		if(debugLevel < 3 && jt->backlink)
			delete_property_win(jt->f, jt->backlink);
		t = jt->t;
		delFromList(jt);
		nzFree(jt->backlink);
		nzFree(jt);
		if(t) {
// this will free the xhr object and allow for garbage collection.
			disconnectTagObject(t);
			t->dead = true;
			t->action = TAGACT_NOP;
		}
	} else {
		int n = jt->jump_sec * 1000 + jt->jump_ms;
		if (n < timerSpread)
			n = timerSpread;
		if(timerspeed > 1 && !jt->pending && !jt->t &&
		0x40000000 / timerspeed >= n)
			n *= timerspeed;
		jt->sec = now_sec + n / 1000;
		jt->ms = now_ms + n % 1000;
		if (jt->ms >= 1000)
			jt->ms -= 1000, ++jt->sec;
	}
}

void showTimers(void)
{
	const struct jsTimer *t;
	int n;
	bool printed = false;

	currentTime();
	foreach(t, timerList) {
		if(t->pending)
			continue;
		if(t->f->owner != cw)
			continue;
		printed = true;
		if(t->isInterval)
			printf("interval ");
		else if(t->t)
			printf("%s ",
			   (t->t->action == TAGACT_SCRIPT ? "script" : "xhr"));
		else
			printf("timer ");
		printf("%d cx%d %s ", t->tsn, t->f->gsn, t->backlink);
		n = (t->sec - now_sec) * 1000;
		n += t->ms - now_ms;
		if(n >= 1000 || n < -1000)
			printf("in %ds", n / 1000);
		else
			printf("in %dms", n);
		if(t->isInterval) {
			n = t->jump_sec * 1000 + t->jump_ms;
			if(n >= 1000)
				printf(" freq %ds", n / 1000);
			else
				printf(" freq %dms", n);
		}
		puts("");
	}

	if(!printed)
		i_puts(MSG_Empty);
}

void domOpensWindow(const char *href, const char *name)
{
	char *r;
	const char *a;
	bool replace = false;
	int ctx;
	Frame *f;

// replace and context number are packed into the url
	if (*href == 'r') replace = true;
	++href;
	ctx = strtol(href, (char**)&href, 10);
	if (!*href) {
		debugPrint(3, "javascript is opening a blank window");
		return;
	}
	f = frameFromWindow(ctx);
	if(!f) {
		debugPrint(1, "redirect %s abort no frame for context %d", href, ctx);
		return;
	}

	r = resolveURL(cf->hbase, href);
	if(!f->browseMode) goto create_hyperlink;

	if(!replace && f != &cw->f0) {
		debugPrint(1, "subframe %d suggestss you go to a new web page %s", ctx, r);
		nzFree(r);
		return;
	}

	gotoLocation(r, 0, replace, f);
	return;

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

/* the new string, the result of the render operation */
static char *ns;
static int ns_l;
static bool tdfirst;
static int invisible;
static Tag *inv2;	// invisible via css
/* 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, "≫");
	}
}

// look for table winthin table, recursive
static bool tableUnder(const Tag *top)
{
	const Tag *t;
	for(t = top->firstchild; t; t = t->sibling) {
		if(t->action == TAGACT_TABLE) return true;
		if(tableUnder(t)) return true;
	}
	return false;
}

/*********************************************************************
Is this table a matrix of data, or just for layout purposes?
0 means we can't tell, 1 is data, 2 is presentation
t is a cell or a row.
This is pretty slow, so when we get an answer we remember it.
Then we can refer to it on the next cell.
This means javascript can't change it after the fact; it's type is set.
I use the much overloaded variable lic for the type,
and post to indicate it was set.
*********************************************************************/

int tableType(const Tag *t)
{
	const char *role;
Tag *v; // table tag above
	const Tag *b; // tbody
// find the containing table
	for(v = (Tag*)t; v; v = v->parent)
		if(v->action == TAGACT_TABLE) break;
	if(!v) return 0; // this should never happen
	if(v->post) return v->lic;

	role = attribVal(v, "role");
	if(stringEqual(role, "presentation")) goto presentation;
	if(stringEqual(role, "table")) goto data;
// if this table contains another one it has to be presentation
	if(tableUnder(v)) goto presentation;
// if there is a caption or thead section, probably data
	for(b = v->firstchild; b; b = b->sibling) {
		if(b->action == TAGACT_THEAD ||
		stringEqual(b->info->name, "caption"))
			goto data;
		if(b->action == TAGACT_TBODY) break;
	}
	if(!b) b = v;
// check and see if first row has at least two headings
	if((t = b->firstchild) && t->action == TAGACT_TR
	&& (t = t->firstchild) && t->action == TAGACT_TD
	&& t->info->name[1] == 'h' && (t = t->sibling)
	&& t->action == TAGACT_TD && t->info->name[1] == 'h')
		goto data;

// I give up
	v->post = true;
	return (v->lic = 0);

data:
	v->post = true;
	return (v->lic = 1);

presentation:
	v->post = true;
	return (v->lic = 2);
}

static char *td_text;
static int td_text_l;
static void td_textUnder(const Tag *u)
{
	if(u->action == TAGACT_TEXT && u->textval)
		stringAndString(&td_text,&td_text_l, u->textval);
	for(u = u->firstchild; u; u = u->sibling)
		td_textUnder(u);
}

// the whole row is <th>, and there's more than one.
bool all_th(const Tag *tr)
{
	const Tag *th;
	int j = 0;
	for(th = tr->firstchild; th; th = th->sibling) {
		if(th->action != TAGACT_TD) continue;
		if(th->info->name[1] == 'd') return false;
		++j;
	}
	return j > 1;
}

// return a column heading by number.
// This function and the next two are used to display an unfolded row.
static void findHeading(const Tag *t, int colno)
{
	int j = 1;
	const Tag *u;
	bool ishead = false;
	td_text = initString(&td_text_l);
	if(!t->parent ||
	((t = t->parent)->action != TAGACT_TABLE &&
	t->action != TAGACT_THEAD &&
	t->action != TAGACT_TBODY &&
	t->action != TAGACT_TFOOT))
		return;
// can't unfold the header row
	if(t->action == TAGACT_THEAD) return;
	if(t->action == TAGACT_TBODY || t->action == TAGACT_TFOOT) {
		t = t->parent;
		if(!t || t->action != TAGACT_TABLE)
			return;
	}
	t = t->firstchild;
	for(u = t; u; u = u->sibling)
		if(u->action == TAGACT_THEAD || u->action == TAGACT_TBODY) {
			t = u;
			break;
		}
	if(t && (t->action == TAGACT_THEAD || t->action == TAGACT_TBODY)) {
		ishead = (t->action == TAGACT_THEAD);
		t = t->firstchild;
	}
	if(!t || t->action != TAGACT_TR || !t->firstchild)
		return;
// it's a row with stuff under it
	t = t->firstchild;
	if(t->action != TAGACT_TD ||
	(!ishead && t->info->name[1] != 'h'))
		return;

// t is first cell in row, and is <th> or is inside <thead>
// Make sure it's not a row header in <tbody>
// continue if the whole row is <th>
	if(!ishead && !all_th(t->parent))
		return;

	while(t) {
		if(t->action == TAGACT_TD) {
			j += t->js_ln;
			if(j > colno) {
// this is the header we want, descend to the text field
				td_textUnder(t);
				return;
			}
		}
		t = t->sibling;
	}
	return;
}

static void headingAndData(int j, const Tag *tr, int td_n, int ttype, bool closerow)
{
	if(tr->ur) {
		findHeading(tr, j);
		if(td_text_l) {
			stringAndString(&ns, &ns_l, td_text);
			nzFree(td_text);
		} else stringAndNum(&ns, &ns_l, j);
		stringAndString(&ns, &ns_l, ": ");
	}
	if(!td_n) return;
	if(ttype != 1) {
		stringAndString(&ns, &ns_l, "↑\n");
		return;
	}
	if(closerow && !tr->ur)
		stringAndChar(&ns, &ns_l, DataCellChar);
	td_text = initString(&td_text_l);
	td_textUnder(tagList[td_n]);
	if(td_text_l) {
		stringAndString(&ns, &ns_l, td_text);
		nzFree(td_text);
	}
	if(tr->ur)
		stringAndChar(&ns, &ns_l, '\n');
	if(!closerow && !tr->ur)
		stringAndChar(&ns, &ns_l, DataCellChar);
}

static void td2columnHeading(const Tag *tr, const Tag *td)
{
// unusual situations can cause tr to be 0
// <html></html><td></td>
	if(!tr) return;

	const Tag *v;
	int j = 1, seqno, ics;
	char *prior_p, *last_p;
	int prior_j = 0;
	uchar ttype = tableType(tr);
	char *cs = tr->js_file; // the cellstring
	if(!cs) cs = emptyString;
	last_p = cs;

// don't do anything for an inline table that is not data.
	if(!tr->ur && ttype != 1) return;

	for(v = tr->firstchild; v; v = v->sibling) {
		if(v->action != TAGACT_TD) continue;
		prior_p = 0;
		while(isdigitByte(*cs)) {
			if(!prior_p) prior_p = cs, prior_j = j;
			seqno = strtol(cs, &cs, 10);
			ics = 1;
			if(*cs == '@') ics = strtol(cs + 1, &cs, 10);
			++cs; // skip past comma
			j += ics;
		}
// the comma that stands in for this <td> cell
// could be @3,
		cs = strchr(cs, ',');
		cs = (cs ? cs + 1 : emptyString);
		if(v == td) break;
		j += v->js_ln;
		last_p = cs;
	}

	if(td && !v) return; // should never happen
	if(!v) {
// reference rowspan fields from above that slide in
// at the end of the row.
		cs = last_p;
		while(*cs) {
			if(*cs == ',') { ++cs, ++j; continue; }
			if(*cs == '@') {
				ics = strtol(cs + 1, &cs, 10);
				j += ics, ++cs;
				continue;
			}
			if(!isdigitByte(*cs)) break; // should be a number
			seqno = strtol(cs, &cs, 10);
			ics = 1;
			if(*cs == '@') ics = strtol(cs + 1, &cs, 10);
			++cs; // skip past comma
			headingAndData(j, tr, seqno, ttype, true);
			j += ics;
		}
		return;
	}

// reference rowspan fields from above that slide in
// just before this cell.
	if(prior_p) {
		cs = prior_p;
		while(isdigitByte(*cs)) {
			seqno = strtol(cs, &cs, 10);
			ics = 1;
			if(*cs == '@') ics = strtol(cs + 1, &cs, 10);
			++cs; // skip past comma
			headingAndData(prior_j, tr, seqno, ttype, false);
			prior_j += ics;
		}
	}

	if(!tr->ur) return;

	headingAndData(j, tr, 0, 0, false);
	if(td->js_ln > 1) {
// replace : with double arrow
		ns[ns_l -= 2] = 0;
		stringAndString(&ns, &ns_l, " ⇔ ");
		headingAndData(j + td->js_ln - 1, tr, 0, 0, false);
	}
}

// return allocated string, as it may come from js
static char *arialabel(const Tag *t)
{
	const char *a;
	if(allowJS && t->jslink) {
		char *u = get_property_string_t(t, "aria-label");
		if(u && *u)
			return u;
		nzFree(u);  // empty string?
	}
	a = attribVal(t, "aria-label");
	return (a && *a) ? cloneString(a) : 0;
}

static int ariaHeadingLevel(const Tag *t)
{
	const char *role, *lvl;
	int n;
#if 0
// This works on the attribute system, not on properties,
// look into this later.
	if(allowJS && t->jslink) {
		char *r = get_property_string_t(t, "role");
		if(stringEqual(r, "heading")) {
			nzFree(r);
			char *l = get_property_string_t(t, "aria-level");
			n = l ? atoi(l) : 2;
			nzFree(l);
			if(n < 1) n = 1;
			if(n > 6) n = 6;
			return n;
		}
		nzFree(r);
	}
#endif
	role = attribVal(t, "role");
	if(!stringEqual(role, "heading")) return 0;
	lvl = attribVal(t, "aria-level");
	n = lvl ? atoi(lvl) : 2;
	if(n < 1) n = 1;
	if(n > 6) n = 6;
	return n;
}

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

// this routine guards against <td onclick=blah><a href=#>stuff</a></td>
// showing up as {{stuff}}
static int ahref_under(const Tag *t)
{
	Tag *u;
	int rc;
	for(u = t->firstchild; u; u = u->sibling) {
		if(u->action == TAGACT_A) {
			if (!u->href && u->jslink) {
				char *new_url = get_property_url_t(u, false);
				if (new_url && *new_url)
					nzFree(u->href), u->href = new_url;
			}
			if (!u->href && u->jslink && handlerPresent(u, "onclick"))
				u->href = cloneString("#");
			if(u->href) return 1;
		}
		if(u->action == TAGACT_TEXT) {
			if (u->jslink) {
// defer to the javascript text.
				char *w = get_property_string_t(u, "data");
				if (w)
					nzFree(u->textval), u->textval = w;
			}
			const char *s = u->textval;
			if(!s) s = emptyString;
			while(*s) { if(!isspaceByte(*s)) return -1; ++s; }
			continue;
		}
		rc = ahref_under(u);
		if(rc) return rc;
	}
	return 0;
}

static void emphasize(Tag *t, bool opentag, const char *a)
{
	const Tag *u;
	char mark[12];
	sprintf(mark, "%c@%s", (opentag ? '`' : '\''), a);
	int l = strlen(a);

// I don't see the point of injecting these marks if we are
// inside a hyperlink or button.
	Tag *t2 = findOpenTag(t, TAGACT_A);
	if(t2 && t2->href) return;
	t2 = findOpenTag(t, TAGACT_INPUT);
	if(t2 && t2->info->name[0] != 'b') return;
	t2 = findOpenTag(t, TAGACT_OPTION);
	if(t2) return;

// the converse; don't emphasize if we are a hyperlink.
    for(u = t->firstchild; u; u = u->sibling) {
        if(isBlankTag(u)) continue;
        if(u->action != TAGACT_A || !u->href) break;
    }
    if(!u) return;

	if(opentag) {
// see if we can compress adjacent blocks of emphasized text
		char *u = ns + ns_l;
		while(u > ns && u[-1] == ' ') --u;
		if(u - ns >= 2 + l &&
		u[-(l+2)] == '\'' &&
		u[-(l+1)] == '@' &&
		!memcmp(u - l, a, l)) {
			memmove(u - (l+2), u, ns + ns_l - u);
			ns_l -= (2+l);
			return;
		}
	}
	stringAndString(&ns, &ns_l, mark);
}

static Tag *deltag;
// the text decoration characters
static char  tdchars[8][8] =  {
    "x", "x", "x", "x", "x", "x", "x", "x", 
};

int is_tdchar(const char *s)
{
    int i;
    for(i = 0; i < 8; ++i) {
        const char *a = tdchars[i];
        int l = strlen(a);
        if(!strncmp(a, s, l)) return l;
    }
    return 0;
}

bool set_tdchars(const char *s)
{
    int n, j;
    unsigned char c, d;
    for(n = 0; n < 8; ++n)
        tdchars[n][0] = 'x', tdchars[n][1] = 0;
    n = 0;
    while(n < 8) {
        c = *s++;
        if(!c) break; // end of string
        if(c <= ' ') return false; // control character
        if(!(c & 0x80)) { // ascii
            if(isalnum(c) && c != 'x') return false;
            tdchars[n++][0] = c;
            continue;
        }
        if(!(c & 0x40)) return false; // bad utf8
        tdchars[n][0] = c;
        c <<= 1, j = 1;
        while(c & 0x80) {
            d = *s++;
            if((d & 0xc0) != 0x80) goto badstring;
            tdchars[n][j++] = d;
            c <<= 1;
        }
        tdchars[n++][j] = 0;
    }
    return true;
badstring:
    tdchars[n][0] = 'x', tdchars[n][1] = 0;
        return false;
}

static void renderNode(Tag *t, bool opentag, struct parseContext *pc)
{
	int tagno = t->seqno;
	char hnum[40];		// hidden number
#define ns_hnum() stringAndString(&ns, &ns_l, hnum)
#define ns_ic() stringAndChar(&ns, &ns_l, InternalCodeChar)
#define checkDisabled(u) if(inputDisabled(u) || (u->itype > INP_SUBMIT && inputReadonly(u))) stringAndString(&ns, &ns_l, "🛑")
	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;

        (void) pc;
	debugPrint(6, "rend %c%s", (opentag ? ' ' : '/'), ti->name);
	if(opentag) ++rrcount;

	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) {
		debugPrint(6, "tag is deleted");
		deltag = t;
		goto li_hide;
	}

	if (inv2) {
		if (inv2 == t)
			inv2 = NULL;
		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; isdigitByte(*u3); ++u3) ;
				if (*u3 == '*' && !*++u3)
					break;
			}
			if (!isspaceByte(*u3))
				goto yescolorend;
		}
		u0 = backColon(u1);
		if (*u0 != ':')
			goto yescolorend;
		for (u3 = u0 + 1; u3 < u1; ++u3)
			if (*u3 != ' ' && !isalphaByte(*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;

// We will call eb$visible to derive visibility, that in turn invokes css,
// that in turn requires us to be in the frame of the tags we are looking at.
// render(), which calls this, restores cf.
	cf = t->f0;

	if (opentag) {
// what is the visibility now?
		uchar v_now = 2;
		if(allowJS && t->jslink) {
			t->disval = 0;
// lots of text nodes, and they don't carry visibility information
			if(action != TAGACT_TEXT)
				t->disval =
				    run_function_onearg_win(cf, "eb$visible", t);
			if(t->disval)
				debugPrint(4, "tag %s.%d visibility %d",
				t->info->name, t->seqno, t->disval);
// If things appear upon hover, they do this sometimes if your mouse
// is anywhere in that section, so maybe we should see them.
// Also if color is transparent then it surely changes to a color
// if the mouse is somewhere or on some circumstance,
// so just bring this to light as well.
			if(t->disval != DIS_INVISIBLE)
				t->disval = DIS_COLOR;
		} else {
// allow html to hide sections, even if js is not running.
			t->disval = 0;
			if(((a = attribVal(t, "hidden")) && !stringEqual(a, "false")) ||
			((a = attribVal(t, "aria-hidden")) && !stringEqual(a, "false")))
				t->disval = DIS_INVISIBLE;
		}
		if (t->disval == DIS_INVISIBLE)
			v_now = DIS_INVISIBLE;
		if (t->disval == DIS_HOVER)
			v_now = DIS_COLOR;
		if (t->action == TAGACT_TEXT && v_now == DIS_HOVER) {
			Tag *y = t;
			while (y && y->f0 == cf) {
				uchar dv = y->disval;
				if (dv == DIS_TRANSPARENT)
					v_now = DIS_INVISIBLE;
				if (dv == DIS_HOVERCOLOR)
					v_now = DIS_COLOR;
				if (dv >= DIS_COLOR)
					break;
				y = y->parent;
			}
		}
// gather some stats
		if (v_now == DIS_INVISIBLE)
			++invcount;
		if (v_now == DIS_COLOR)
			++hovcount;
		if (v_now == DIS_INVISIBLE && !showall) {
			inv2 = t;
			return;
		}
		if (!showall && v_now == DIS_COLOR && !activeBelow(t)) {
			inv2 = t;
			return;
		}
		if (action == TAGACT_TEXT && t->jslink &&
		    get_property_bool_t(t, "inj$css")) {
			++injcount;
			if (!showall) {
				inv2 = t;
				return;
			}
		}
	}

	retainTag = true;
	if (invisible)
		retainTag = false;
	if (ti->bits & TAG_INVISIBLE) {
		if(opentag) debugPrint(6, "tag is invisible");
		else debugPrint(6, "invisible tag is closed");
		retainTag = false;
		opentag ? ++invisible : --invisible;
/* special case for noscript with no js */
		if (action == TAGACT_NOSCRIPT && !cf->cx)
			invisible = false;
	}

	if (retainTag && doColors && opentag) {
		char *u0, *u1, *u2, *u3;
		char *color, *recolor = 0;
		t->iscolor = false;
		color = get_style_string_t(t, "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++ != ':' ||
		    (unsigned)(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; isdigitByte(*u3); ++u3) ;
			if (isalnumByte(*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->jslink) {
// 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 Text.data assignment has a side effect.
			char *u = get_property_string_t(t, "data");
			if (u)
				nzFree(t->textval), t->textval = u;
		}
		if (!t->textval) break;
		if (!invisible) {
// I'm not gonna include the node numbers for all the text nodes;
// a lot of text nodes are whitespace and the tag numbers just confuse things.
// This assumes you're not goint to jump to a text node,
// or otherwise interact with it from the command line.
//			tagInStream(tagno);
			stringAndString(&ns, &ns_l, t->textval);
		}
		break;

	case TAGACT_CDATA:
		if (!t->textval) break;
		if (!invisible) {
			stringAndString(&ns, &ns_l, t->textval);
		}
		break;

	case TAGACT_A:
// special code for attached images from an email
		if(opentag && !attimg &&
		(a = attribVal(t, "attimg")) &&
		*a == 'y') {
			deltag = t;
			break;
		}
		currentA = (opentag ? t : 0);
		if (!retainTag) break;
		int arialevel_a = ariaHeadingLevel(t);
// Javascript might have set this url.
		if (opentag && !t->href && t->jslink) {
			char *new_url = get_property_url_t(t, 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((a = arialabel(t))) {
// for <a>,  aria-label replaces anything that was below; this takes precedence
					ns_hnum();
					stringAndString(&ns, &ns_l, a);
					cnzFree(a);
					sprintf(hnum, "%c0}", InternalCodeChar);
					ns_hnum();
					deltag = t;
					break;
				} else if (t->jslink     && (a =
					get_property_string_t(t, "title"))) {
// <a title=x>   x appears on hover
					++hovcount;
					if (showall) {
						stringAndString(&ns, &ns_l, a);
						stringAndChar(&ns, &ns_l, ' ');
					}
					cnzFree(a);
				}
			} else // open or closed
				sprintf(hnum, "%c0}", InternalCodeChar);
		} else { // href or no href
			if (opentag)
				sprintf(hnum, "%c%d*", InternalCodeChar, tagno);
			else
				hnum[0] = 0;
		} // href or no href
		if(arialevel_a && opentag) {
			stringAndChar(&ns, &ns_l, '\f');
			char hbuf_a[8];
			sprintf(hbuf_a, "h%d ", arialevel_a);
			stringAndString(&ns, &ns_l, hbuf_a);
		}
		ns_hnum();
		if(arialevel_a && !opentag)
			stringAndChar(&ns, &ns_l, '\f');
		if (endcolor)
			swapArrow();
		break;

// check for span onclick and make it look like a link.
// Same for div, maybe for others too.
	case TAGACT_SPAN: case TAGACT_DIV:
		a = 0; { // satisfy the compiler and scope the next 3 variables
// next three variables will remain null if opentag is false
		char *al = opentag ? arialabel(t) : 0;
		const char *tit1 = 0;
		char *tit2 = 0;
// If nothing in the span then the title becomes important.
		if (!t->firstchild && opentag && !al) {
			tit1 = attribVal(t, "title");
			if (allowJS && t->jslink)
				tit2 = get_property_string_t(t, "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.
		if (!t->onclick && opentag && t->jslink && handlerPresent(t, "onclick"))
			t->onclick = true;
		if (!(t->onclick & allowJS) || ahref_under(t) > 0) {
// regular span, don't need title unless it is inside a link
			if(opentag && !findOpenTag(t, TAGACT_A))
				tit1 = 0, nzFree0(tit2);
			if((al || tit1 || tit2) && action == TAGACT_DIV)
				stringAndChar(&ns, &ns_l, '\n');
			j = ns_l;
			if (al) // aria-label
				stringAndString(&ns, &ns_l, al), nzFree(al);
			if (tit2) // allocated title
				stringAndString(&ns, &ns_l, tit2), nzFree(tit2);
			else if (tit1)
				stringAndString(&ns, &ns_l, tit1);
			if(ns_l > j && t->firstchild)
				stringAndChar(&ns, &ns_l, ' ');
			goto nop;
		}
// this span has click, so turn into {text}
		if (opentag) {
			sprintf(hnum, "%c%d{", InternalCodeChar, tagno);
			ns_hnum();
			if((al || tit1 || tit2) && action == TAGACT_DIV)
				stringAndChar(&ns, &ns_l, '\n');
			j = ns_l;
			if (al)
				stringAndString(&ns, &ns_l, al), nzFree(al);
			if (tit2) // allocated title
				stringAndString(&ns, &ns_l, tit2), nzFree(tit2);
			else if (tit1)
				stringAndString(&ns, &ns_l, tit1);
			if((ns_l > j) && t->firstchild)
				stringAndChar(&ns, &ns_l, ' ');
		} else {
			sprintf(hnum, "%c0}", InternalCodeChar);
			ns_hnum();
			if (endcolor)
				swapArrow();
		}
		}break;

	case TAGACT_BQ:
		if (invisible)
			break;
		while(ns_l && isspaceByte(ns[ns_l-1])) --ns_l;
// compress adjacent blockquotes   '' ``
		if(opentag && ns_l >= 2 && ns[ns_l-1] == '\'' && ns[ns_l-2] == '\'') {
			ns_l--;
			ns[ns_l-1] = '\f';
			break;
		}
		stringAndString(&ns, &ns_l,
		(opentag ? "\f``" : "''\f"));
		break;

/*********************************************************************
Here are some tags that change the appearance of the text, in order
to provide semantic meaning. e.g. <em>emphasized text</em>
We aren't going to distinguish between all such possible tags.
Bold, strong, emphasized, all mean essentially the same thing.
We don't want to overwhelm the user with a flurry of punctuations.
We inject the text mark, but no tag. That means <em id=hello> won't work.
Tags in the stream would make it much harder to swap marks and whitespace.
I don't believe it is correct html or javascript to put attributes on these tags.
If somebody does, and it is essential to the operation of the website,
we'll have to rethink the matter.
Marks are `@*  text  '@*, or other characters in place of *.
This makes them unambiguous in the stream.
No false positives, as you would surely encounter with a single *.
Eventually the `@ and '@ are crunched away.
*********************************************************************/

    case TAGACT_EM: case TAGACT_STRONG: case TAGACT_B: case TAGACT_I:
    case TAGACT_U: case TAGACT_INS: case TAGACT_S: case TAGACT_DEL:
// TAGACT_CODE could go here, but it's more like blockquote than
// an emphasized word or phrase.
        if (invisible || !textDecorateOK()) break;
        a = tdchars[action - TAGACT_EM];
        if(*a == 'x') break; // x is code for no action
// check for the tags that produce the same symbol
        for(j = 0; j < 8; ++j)
            if(!strcmp(a, tdchars[j]) &&
            findOpenTag(t, TAGACT_EM + j))
                return;
        emphasize(t, opentag, a);
        break;

	case TAGACT_SVG:
		if (!invisible && opentag) {
// I use to print "graphics" here, but that conveys virtually no information.
// Maybe at some point I'll find something useful to insert
// to say yes there's some visual thing here.
// Meantime, I better at least put in a space, because some graphic
// might separate two words.
			stringAndChar(&ns, &ns_l, ' ');
		}
		break;

	case TAGACT_OL:
	case TAGACT_UL:
		t->lic = t->slic;
		t->post = false;
// If this is <ul> with one item or no items below,
// and in the cell of a data table,
// indicate with lic = -2. We will suppresss it.
		j = 0;
		for(ltag = t->firstchild; ltag; ltag = ltag->sibling)
			if(ltag->action == TAGACT_LI) ++j;
		if(j <= 1 && action == TAGACT_UL &&
		t->parent && t->parent->action == TAGACT_TD &&
		tableType(t->parent) == 1) {
			t->lic = -2;
			break;
		}
// falling through is just fine, but sometimes generates a cc warning, so...
		goto nop;

	case TAGACT_DD:
		stringAndString(&ns, &ns_l,
		opentag ? " \r: " : "\r");
		goto check_id;

	case TAGACT_DL:
	case TAGACT_DT:
	case TAGACT_OBJECT:
	case TAGACT_BR:
	case TAGACT_P:
	case TAGACT_H:
	case TAGACT_BODY:
	case TAGACT_LABEL:
	case TAGACT_NOP:
nop:
		if (invisible)
			break;
		j = ti->para;
		if (opentag)
			j &= 3;
		else
			j >>= 2;

		int arialevel = (action != TAGACT_H) ? ariaHeadingLevel(t) : 0;
		if(arialevel && !j) j = 2;

// special code for div inside a header, which shouldn't happen but does
		if(action == TAGACT_DIV && findOpenTag(t, TAGACT_H))
			j = 0;

// defense against <td><p>stuff</p></td>
// or even <td><i><font size=-1><p>stuff</p></font></i></td>
// Suppress linebreak if this is first or last child of a cell.
		if(j) {
			const Tag *y = t, *z, *x;
			while((z = y->parent)) {
// h3 inside a cell, table is almost certainly for presentation
// We see this on facebook.
// Let's hope the tableType() catches it.
//				if(y->action == TAGACT_H) goto past_cell_paragraph;
/* I use to say, if not <p> first or </p> last, then abort
				if(opentag && z->firstchild != y) goto past_cell_paragraph;
				if(!opentag && y->sibling) goto past_cell_paragraph;
But I've added some code, probably just for one website, but oh well.
Defense against <td><span></span><p>stuff</p></td>
Should we watch for empty-ish tags besides span, or even empty trees? */
				if(opentag) {
					for(x = z->firstchild; x != y; x = x->sibling)
						if(x->firstchild || x->action != TAGACT_SPAN)
							goto past_cell_paragraph;
				}
				if(!opentag) {
					for(x = y->sibling; x; x = x->sibling)
						if(x->firstchild || x->action != TAGACT_SPAN)
							goto past_cell_paragraph;
				}
				if(z->action == TAGACT_TD) break;
				y = z;
			}
			if(z && tableType(z) == 1) j = 0;
		}
past_cell_paragraph:

		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 || arialevel)) {
// no need to print h1 if the section is empty
		        for(ltag = t->firstchild; ltag; ltag = ltag->sibling)
		            if(!isBlankTag(ltag)) break;
		        if(ltag) {
		            if(arialevel)
		                sprintf(hnum, "h%d ", arialevel);
		            else {
		                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)
			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 : tagno + 1);
// I need to manage the paragraph breaks here, rather than ti->para,
// which would rule if I simply redirected to nop.
// But the order is wrong if I do that.
// This can be suppressed by <pre nowspc>
		if (opentag && !attribVal(t, "nowspc"))
			stringAndChar(&ns, &ns_l, '\f');
		sprintf(hnum, "%c%d*", InternalCodeChar, j);
		ns_hnum();
		if (!opentag && !attribVal(t, "nowspc"))
			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(ti->name, "button"))
				break;
// <button></button> with no text yields "push".
			j = 0;
			while (ns_l && isspaceByte(ns[ns_l - 1]))
				--ns_l, j = 1;
			if (ns_l >= 3 && ns[ns_l - 1] == '<'
			    && isdigitByte(ns[ns_l - 2]))
				stringAndString(&ns, &ns_l,
						i_message(MSG_Push));
			ns_ic();
			stringAndString(&ns, &ns_l, "0>");
			if (endcolor) swapArrow();
			checkDisabled(t);
			if(j) stringAndChar(&ns, &ns_l, ' ');
			break;
		}
// value has to be something.
		if (!t->value)
			t->value = emptyString;
		itype = t->itype;
		if (itype == INP_HIDDEN)
			break;
		if (itype == INP_TA && t->lic >= 0) {
			j = t->lic;
			if (j)
				sprintf(hnum, "%c%d<session %d%c0>",
					InternalCodeChar, t->seqno, j,
					InternalCodeChar);
			else if (t->value[0])
				sprintf(hnum, "%c%d<session text%c0>",
					InternalCodeChar, t->seqno,
					InternalCodeChar);
			else
				sprintf(hnum, "%c%d<session ?%c0>",
					InternalCodeChar, t->seqno,
					InternalCodeChar);
			ns_hnum();
			checkDisabled(t);
			break;
		}
		sprintf(hnum, "%c%d<", InternalCodeChar, tagno);
		ns_hnum();
// button stops here, until </button>
		if (stringEqual(ti->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) {
				a = imageAlt(t);
				if(!a) a = (char *)i_message(MSG_Submit);
				stringAndString(&ns, &ns_l, a);
			} else if (itype == INP_RESET)
				stringAndString(&ns, &ns_l,
						i_message(MSG_Reset));
			else if (itype == INP_BUTTON)
				stringAndString(&ns, &ns_l,
						i_message(MSG_Push));
		} else {
// in case js checked or unchecked
			if (allowJS && t->jslink)
				t->checked =
				    get_property_bool_t(t, "checked");
			stringAndChar(&ns, &ns_l, (t->checked ? '+' : '-'));
		}
		if (currentForm && (itype == INP_SUBMIT || itype == INP_IMAGE)) {
			if (currentForm->bymail)
				stringAndString(&ns, &ns_l,
						i_message(MSG_Bymail));
		}
		ns_ic();
		stringAndString(&ns, &ns_l, "0>");
		checkDisabled(t);
		break;

	case TAGACT_LI:
// we are only here on opentag. Look for containing <ol> or <ul>
		if (!(ltag = findOpenList(t)))
			goto nop;
		if(ltag->lic == -2) break; // suppressed
		char olbuf[32];
		olbuf[0] = 0;
		tagInStream(tagno);
		if (ltag->action == TAGACT_OL) {
			j = ++ltag->lic;
// this checks for <li value=7>, but does not check for javascript
// dynamically setting value after the html is parsed.
// Add that in someday.
			if(t->value && (l = stringIsNum(t->value)) >= 0)
				ltag->lic = j = l;
			sprintf(olbuf, "%d. ", j);
		} else if(ltag->lic == 0) {
			strcpy(olbuf, "* ");
		}
		if (!invisible)
			stringAndString(&ns, &ns_l, olbuf);
		stringAndChar(&ns, &ns_l, '\n');
		break;

	case TAGACT_HR:
// <hr> can be in the midst of options, as a separator between options.
// We sure don't want that here.
		if(findOpenTag(t, TAGACT_INPUT)) break;
		if (retainTag) {
			tagInStream(tagno);
			stringAndString(&ns, &ns_l, "\r----------\r");
		}
		break;

	case TAGACT_TR:
		if (!t->onclick && opentag && t->jslink && handlerPresent(t, "onclick"))
			t->onclick = true;
		if (opentag)
			tdfirst = true;
		if(t->ur && opentag && (ltag = t->parent)
		&& (ltag->action == TAGACT_TABLE || ltag->action == TAGACT_TBODY
		|| ltag->action == TAGACT_TFOOT)) {
// print the row number
			const Tag *v = ltag->firstchild;
			j = 1;
			while(v && v != t) {
				if(v->action == TAGACT_TR) {
// sometimes the headers are in the tbody and not in thead.
					if(j == 1 && all_th(v))
						; // skip <th> row
					else
						++j;
				}
				v = v->sibling;
			}
			if(v) { // should always happen
				char rowbuf[24];
				if(t->onclick) {
					sprintf(rowbuf, "%c%d{row %d%c0}\n",
					InternalCodeChar, tagno, j, InternalCodeChar);
					stringAndString(&ns, &ns_l, rowbuf);
				} else {
					tagInStream(tagno);
					sprintf(rowbuf, "row %d\n", j);
					stringAndString(&ns, &ns_l, rowbuf);
				}
				break;
			}
		}
		if(opentag && t->onclick) {
// put {row} in front
			char rowbuf[24];
			sprintf(rowbuf, "%c%d{row%c0}:",
			InternalCodeChar, tagno, InternalCodeChar);
			stringAndString(&ns, &ns_l, rowbuf);
			break;
		}
		if(!opentag && (ltag = t->parent)
		&& (ltag->action == TAGACT_TABLE || ltag->action == TAGACT_TBODY
		|| ltag->action == TAGACT_TFOOT)) {
			if(t->ur) {
				if (tdfirst) tdfirst = false;
				else stringAndChar(&ns, &ns_l, '\n');
			}
			td2columnHeading(t, 0);
		}

	case TAGACT_TABLE:
		goto nop;

	case TAGACT_TD:
		if (!retainTag)
			break;
		if (!t->onclick && opentag && t->jslink && handlerPresent(t, "onclick"))
			t->onclick = true;
		if(!opentag) {
			if(t->onclick && ahref_under(t) <= 0) {
				sprintf(hnum, "%c0}", InternalCodeChar);
				ns_hnum();
			}
			if((t->lic > 1 || t->js_ln > 1) && tableType(t) == 1 && !t->parent->ur) {
				char arrows[20];
				stringAndChar(&ns, &ns_l, ' ');
				arrows[0] = 0;
				if(t->lic > 1)
					sprintf(arrows, "↓%d", t->lic);
				if(t->js_ln > 1)
					sprintf(arrows + strlen(arrows), "→%d", t->js_ln);
				stringAndString(&ns, &ns_l, arrows);
			}
			break;
		}
		if(!(ltag = t->parent)
		|| ltag->action != TAGACT_TR || !ltag->ur) {
// Traditional table format, pipe separated,
// on one line if it fits, or wraps in unpredictable ways if it doesn't.
			if (tdfirst)
				tdfirst = false;
			else {
				j = ns_l;
				while (j && ns[j - 1] == ' ')
					--j;
				ns[j] = 0;
				ns_l = j;
				j = tableType(t);
				stringAndChar(&ns, &ns_l, "\3\4 "[j]);
			}
			td2columnHeading(ltag, t);
		} else {
// unfolded row, generate the column heading
			if (tdfirst) tdfirst = false;
			else stringAndChar(&ns, &ns_l, '\n');
			td2columnHeading(ltag, t);
		}
// Always retain the <td> tag, for the ur command.
		if(t->onclick && ahref_under(t) <= 0) {
			sprintf(hnum, "%c%d{", InternalCodeChar, tagno);
				ns_hnum();
		} else {
			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,
				"[", "^(", "`"
			};
			tagInStream(tagno);
			t->lic = ns_l;
			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 indicators */
		while (*u == InternalCodeChar) {
			++u;
			while (isdigitByte(*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;
		}
// frame or area is not expanded
		if (!opentag) break;
		stringAndString(&ns, &ns_l,
				(action == TAGACT_FRAME ? "\rFrame " : "\r"));
// js often creates frames dynamically, so check for src
		if(allowJS && t->jslink) {
			nzFree(t->href);
			t->href = get_property_string_t(t,
			(t->action == TAGACT_AREA ? "href" : "src"));
		}
		a = 0;
		if (action == TAGACT_AREA)
			a = imageAlt(t);
		if (!a) {
			if((a = t->name)) {
				skipWhite2(&a);
				if(*a == '{') {
					const char *b = a + 1;
					skipWhite2(&b);
					if(*b == '"')
						a = "json";
				}
				if(!*a) a = 0;
			}
			if(a && strlen(a) > 40) a = "long";
			if (!a)
				a = altText(t->href);
		}
		if (!a)
			a = (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, a);
		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_DET:
// defer to the javascript, they may have opened the details for us.
		if(t->jslink)
			t->contracted = !get_property_bool_t(t, "open");
		if (!t->contracted) {	/* expanded frame */
			sprintf(hnum, "\r%c%d*%s\r", InternalCodeChar, tagno,
				(opentag ? "`--" : "--`"));
			ns_hnum();
			break;
		}
		if (!opentag) break;
		stringAndString(&ns, &ns_l, "\rDetails ");
		sprintf(hnum, "%c%d{", InternalCodeChar, tagno);
		ns_hnum();
		a = 0;
		for(ltag = t->firstchild; ltag; ltag = ltag->sibling)
			if(ltag->action == TAGACT_SUMMARY) break;
		if(ltag &&
		(ltag = ltag->firstchild) &&
		ltag->action == TAGACT_TEXT)
			a = ltag->textval;
		if(a) stringAndString(&ns, &ns_l, a);
		ns_ic();
		stringAndString(&ns, &ns_l, "0}\r");
		deltag = t;
		break;

	case TAGACT_MUSIC:
		if(!opentag) break;
		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:
		tagInStream(tagno);
		if (!currentA) {
			if (invisible) break;
// images with cid: protocol do not need to appear
// these come from emails, with embedded images
			if(t->href && !strncmp(t->href, "cid:", 4)) break;
			a = imageAlt(t);
			if(a || attimg) {
				stringAndChar(&ns, &ns_l, '[');
				if(a) stringAndString(&ns, &ns_l, a);
				stringAndChar(&ns, &ns_l, ']');
			}
			break;
		}
// image is part of a hyperlink
		if (!retainTag || !currentA->href || currentA->textin)
			break;
		a = imageAlt(t);
		if (!a) a = altText(t->name);
		if (!a) a = altText(currentA->href);
		if (!a) a = altText(t->href);
		if (!a) a = "image";
		stringAndString(&ns, &ns_l, a);
		stringAndChar(&ns, &ns_l, ' ');
		break;

// This is for <unrecognized id=foo> and somewhere else <a href=#foo>
// We have to have this unknown tag in the stream or we can't jump to it.
	default:
check_id:
		if(opentag && t->id)
			tagInStream(tagno);
		break;
	}			// switch

#undef ns_hnum
#undef ns_ic
#undef checkDisabled
}

// We could call render from an inner frame, though it has to render the whole
// page; so save and restore cf.
// This returns an allocated string.
char *render(void)
{
	Frame *f, *save_cf = cf;
	rowspan();
	for (f = &cw->f0; f; f = f->next)
		if (f->cx)
			set_property_bool_win(f, "rr$start", true);
	ns = initString(&ns_l);
	invisible = false;
	inv2 = NULL;
	currentForm = currentA = NULL;
	struct parseContext pc;
	pc.liftup = false;
	pc.callback = renderNode;
	traverseAll(0, &pc);
	cf = save_cf;
	return ns;
}

// 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.
bool itext(int d)
{
	int ln = cw->dot;	// line number
	pst p;			// the raw line to scan
	int n;
	Tag *t;
	char newtext[20], *v = 0;
	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 > 0)
			continue;
// this way works with or without javascript
		if(t->lic < 0 && !(t->jslink && allowJS))
			v = fetchTextVar(t);
		t->lic = sideBuffer(d, (v ? v : t->value), -1, 0);
		nzFree(v);
		change = true;
		sprintf(newtext, "session %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) {
		if(debugLevel > 0)
			displayLine(ln);
		return true;
	}
		setError(inp ? MSG_NoChange : MSG_NoInputFields);
	return false;
}

struct htmlTag *line2tr(int ln)
{
	char *p;
struct htmlTag *t;
	int tagno;

	if(!ln) {
		setError(MSG_EmptyBuffer);
		return 0;
	}
	p = (char *)fetchLine(ln, -1);
	while(*p != '\n') {
		if (*p != InternalCodeChar) { ++p; continue; }
		tagno = strtol((char *)p + 1, (char **)&p, 10);
// could be 0, but should never be negative
		if (tagno <= 0) continue;
		t = tagList[tagno];
		if(t->action == TAGACT_TR)
			return t;
		if(t->action != TAGACT_TD)
			continue;
		t = t->parent;
		if(t && t->action == TAGACT_TR)
			return t;
	}
	setError(MSG_NoTable);
	return 0;
}

static struct htmlTag *line2table(int ln)
{
	struct htmlTag *t;
	if(!ln) {
		setError(MSG_EmptyBuffer);
		return 0;
	}
	t = line2tr(ln);
	if(!t || !t->parent ||
	((t = t->parent)->action != TAGACT_TABLE &&
	t->action != TAGACT_THEAD &&
	t->action != TAGACT_TBODY &&
	t->action != TAGACT_TFOOT)) {
		setError(MSG_NoTable);
		return 0;
	}
	if(t->action != TAGACT_TABLE) {
// it is tbody or thead
		t = t->parent;
		if(!t || t->action != TAGACT_TABLE) {
			setError(MSG_NoTable);
			return 0;
		}
	}
	return t;
}

// This routine is rather unforgiving.
// Has to look like <table><thead><tr><th>text</th>...
// Any intervening tags will throw it off.
// Clearly this routine has to be expanded to cover more html layouts.
bool showHeaders(int ln)
{
	const Tag *t = line2table(ln);
	int colno;
	bool ishead;
	if(!t)
		return false;
	for(t = t->firstchild; t; t = t->sibling)
		if(t->action == TAGACT_THEAD || t->action == TAGACT_TBODY)
			break;
	if(!t)
		goto fail;
	ishead = (t->action == TAGACT_THEAD);
	t = t->firstchild;
	if(!t || t->action != TAGACT_TR || !t->firstchild)
		goto fail;
	t = t->firstchild;
	if(t->action != TAGACT_TD ||
	t->info->name[1] != 'h')
		goto fail;
// if this is tbody, one <th> isn't enough, could be a row header, we need a line of th.
	if(!ishead && !all_th(t->parent)) goto fail;
	colno = 1;
	while(t) {
		if(t->action == TAGACT_TD) {
			printf("%d ", colno);
			colno += t->js_ln; // colspan
			td_text = initString(&td_text_l);
			td_textUnder(t);
			if(!td_text_l) {
				printf("?");
			} else {
				printf("%s", td_text);
				nzFree(td_text);
			}
			nl();
			}
		t = t->sibling;
	}
	return true;

fail:
	setError(MSG_NoColumnHeaders);
	return false;
}

// Parse some html as generated by innerHTML
void html_from_setter(Tag *t, const char *h)
{
	debugPrint(3, "parse html from innerHTML");
	debugPrint(4, "parse under tag %s %d", t->info->name, t->seqno);
	debugGenerated(h);

// Cut all the children away from t
	underKill(t);
	const int startpos = htmlScanner(h, t, true);
	prerender(startpos);
	decorate(startpos, t);
	debugPrint(3, "end parse html from innerHTML");
}

