shithub: purgatorio

ref: 53aac62a54ebbcc1781c0000d3e2384ed038c7bb
dir: /appl/charon/lex.b/

View raw version
implement Lex;

include "common.m";

# local copies from CU
sys: Sys;
CU: CharonUtils;
S: String;
T: StringIntTab;
C: Ctype;
J: Script;
ctype: array of byte;

EOF : con -2;
EOB : con -1;

tagnames = array[] of {
	" ",
	"!",
	"a", 
	"abbr",
	"acronym",
	"address",
	"applet", 
	"area",
	"b",
	"base",
	"basefont",
	"bdo",
	"big",
	"blink",
	"blockquote",
	"body",
	"bq",
	"br",
	"button",
	"caption",
	"center",
	"cite",
	"code",
	"col",
	"colgroup",
	"dd",
	"del",
	"dfn",
	"dir",
	"div",
	"dl",
	"dt",
	"em",
	"fieldset",
	"font",
	"form",
	"frame",
	"frameset",
	"h1",
	"h2",
	"h3",
	"h4",
	"h5",
	"h6",
	"head",
	"hr",
	"html",
	"i",
	"iframe",
	"image",
	"img",
	"input",
	"ins",
	"isindex",
	"kbd",
	"label",
	"legend",
	"li",
	"link",
	"map",
	"menu",
	"meta",
	"nobr",
	"noframes",
	"noscript",
	"object",
	"ol",
	"optgroup",
	"option",
	"p",
	"param",
	"pre",
	"q",
	"s",
	"samp",
	"script",
	"select",
	"small",
	"span",
	"strike",
	"strong",
	"style",
	"sub",
	"sup",
	"table",
	"tbody",
	"td",
	"textarea",
	"tfoot",
	"th",
	"thead",
	"title",
	"tr",
	"tt",
	"u",
	"ul",
	"var",
	"xmp"
};

tagtable : array of T->StringInt;	# initialized from tagnames

attrnames = array[] of {
	"abbr",
	"accept",
	"accept-charset",
	"accesskey",
	"action",
	"align",
	"alink",
	"alt",
	"archive",
	"axis",
	"background",
	"bgcolor",
	"border",
	"cellpadding",
	"cellspacing",
	"char",
	"charoff",
	"charset",
	"checked",
	"cite",
	"class",
	"classid",
	"clear",
	"code",
	"codebase",
	"codetype",
	"color",
	"cols",
	"colspan",
	"compact",
	"content",
	"coords",
	"data",
	"datafld",
	"dataformatas",
	"datapagesize",
	"datasrc",
	"datetime",
	"declare",
	"defer",
	"dir",
	"disabled",
	"enctype",
	"event",
	"face",
	"for",
	"frame",
	"frameborder",
	"headers",
	"height",
	"href",
	"hreflang",
	"hspace",
	"http-equiv",
	"id",
	"ismap",
	"label",
	"lang",
	"language",
	"link",
	"longdesc",
	"lowsrc",
	"marginheight",
	"marginwidth",
	"maxlength",
	"media",
	"method",
	"multiple",
	"name",
	"nohref",
	"noresize",
	"noshade",
	"nowrap",
	"object",
	"onabort",
	"onblur",
	"onchange",
	"onclick",
	"ondblclick",
	"onerror",
	"onfocus",
	"onkeydown",
	"onkeypress",
	"onkeyup",
	"onload",
	"onmousedown",
	"onmousemove",
	"onmouseout",
	"onmouseover",
	"onmouseup",
	"onreset",
	"onresize",
	"onselect",
	"onsubmit",
	"onunload",
	"profile",
	"prompt",
	"readonly",
	"rel",
	"rev",
	"rows",
	"rowspan",
	"rules",
	"scheme",
	"scope",
	"scrolling",
	"selected",
	"shape",
	"size",
	"span",
	"src",
	"standby",
	"start",
	"style",
	"summary",
	"tabindex",
	"target",
	"text",
	"title",
	"type",
	"usemap",
	"valign",
	"value",
	"valuetype",
	"version",
	"vlink",
	"vspace",
	"width"
};

attrtable : array of T->StringInt;	# initialized from attrnames

chartab:= array[] of { T->StringInt
	("AElig",	'Æ'),
	("Aacute",	'Á'),
	("Acirc",	'Â'),
	("Agrave",	'À'),
	("Alpha",	'Α'),
	("Aring",	'Å'),
	("Atilde",	'Ã'),
	("Auml",	'Ä'),
	("Beta",	'Β'),
	("Ccedil",	'Ç'),
	("Chi",	'Χ'),
	("Dagger",	'‡'),
	("Delta",	'Δ'),
	("ETH",	'Ð'),
	("Eacute",	'É'),
	("Ecirc",	'Ê'),
	("Egrave",	'È'),
	("Epsilon",	'Ε'),
	("Eta",	'Η'),
	("Euml",	'Ë'),
	("Gamma",	'Γ'),
	("Iacute",	'Í'),
	("Icirc",	'Î'),
	("Igrave",	'Ì'),
	("Iota",	'Ι'),
	("Iuml",	'Ï'),
	("Kappa",	'Κ'),
	("Lambda",	'Λ'),
	("Mu",	'Μ'),
	("Ntilde",	'Ñ'),
	("Nu",	'Ν'),
	("OElig",	'Œ'),
	("Oacute",	'Ó'),
	("Ocirc",	'Ô'),
	("Ograve",	'Ò'),
	("Omega",	'Ω'),
	("Omicron",	'Ο'),
	("Oslash",	'Ø'),
	("Otilde",	'Õ'),
	("Ouml",	'Ö'),
	("Phi",	'Φ'),
	("Pi",	'Π'),
	("Prime",	'″'),
	("Psi",	'Ψ'),
	("Rho",	'Ρ'),
	("Scaron",	'Š'),
	("Sigma",	'Σ'),
	("THORN",	'Þ'),
	("Tau",	'Τ'),
	("Theta",	'Θ'),
	("Uacute",	'Ú'),
	("Ucirc",	'Û'),
	("Ugrave",	'Ù'),
	("Upsilon",	'Υ'),
	("Uuml",	'Ü'),
	("Xi",	'Ξ'),
	("Yacute",	'Ý'),
	("Yuml",	'Ÿ'),
	("Zeta",	'Ζ'),
	("aacute",	'á'),
	("acirc",	'â'),
	("acute",	'´'),
	("aelig",	'æ'),
	("agrave",	'à'),
	("alefsym",	'ℵ'),
	("alpha",	'α'),
	("amp",	'&'),
	("and",	'∧'),
	("ang",	'∠'),
	("aring",	'å'),
	("asymp",	'≈'),
	("atilde",	'ã'),
	("auml",	'ä'),
	("bdquo",	'„'),
	("beta",	'β'),
	("brvbar",	'¦'),
	("bull",	'•'),
	("cap",	'∩'),
	("ccedil",	'ç'),
	("cdots", '⋯'),
	("cedil",	'¸'),
	("cent",	'¢'),
	("chi",	'χ'),
	("circ",	'ˆ'),
	("clubs",	'♣'),
	("cong",	'≅'),
	("copy",	'©'),
	("crarr",	'↵'),
	("cup",	'∪'),
	("curren",	'¤'),
	("dArr",	'⇓'),
	("dagger",	'†'),
	("darr",	'↓'),
	("ddots", '⋱'),
	("deg",	'°'),
	("delta",	'δ'),
	("diams",	'♦'),
	("divide",	'÷'),
	("eacute",	'é'),
	("ecirc",	'ê'),
	("egrave",	'è'),
	("emdash", '—'),
	("empty",	'∅'),
	("emsp",	' '),
	("endash", '–'),
	("ensp",	' '),
	("epsilon",	'ε'),
	("equiv",	'≡'),
	("eta",	'η'),
	("eth",	'ð'),
	("euml",	'ë'),
	("euro",	'€'),
	("exist",	'∃'),
	("fnof",	'ƒ'),
	("forall",	'∀'),
	("frac12",	'½'),
	("frac14",	'¼'),
	("frac34",	'¾'),
	("frasl",	'⁄'),
	("gamma",	'γ'),
	("ge",	'≥'),
	("gt",	'>'),
	("hArr",	'⇔'),
	("harr",	'↔'),
	("hearts",	'♥'),
	("hellip",	'…'),
	("iacute",	'í'),
	("icirc",	'î'),
	("iexcl",	'¡'),
	("igrave",	'ì'),
	("image",	'ℑ'),
	("infin",	'∞'),
	("int",	'∫'),
	("iota",	'ι'),
	("iquest",	'¿'),
	("isin",	'∈'),
	("iuml",	'ï'),
	("kappa",	'κ'),
	("lArr",	'⇐'),
	("lambda",	'λ'),
	("lang",	'〈'),
	("laquo",	'«'),
	("larr",	'←'),
	("lceil",	'⌈'),
	("ldots", '…'),
	("ldquo",	'“'),
	("le",	'≤'),
	("lfloor",	'⌊'),
	("lowast",	'∗'),
	("loz",	'◊'),
	("lrm",	'‎'),
	("lsaquo",	'‹'),
	("lsquo",	'‘'),
	("lt",	'<'),
	("macr",	'¯'),
	("mdash",	'—'),
	("micro",	'µ'),
	("middot",	'·'),
	("minus",	'−'),
	("mu",	'μ'),
	("nabla",	'∇'),
	("nbsp",	' '),
	("ndash",	'–'),
	("ne",	'≠'),
	("ni",	'∋'),
	("not",	'¬'),
	("notin",	'∉'),
	("nsub",	'⊄'),
	("ntilde",	'ñ'),
	("nu",	'ν'),
	("oacute",	'ó'),
	("ocirc",	'ô'),
	("oelig",	'œ'),
	("ograve",	'ò'),
	("oline",	'‾'),
	("omega",	'ω'),
	("omicron",	'ο'),
	("oplus",	'⊕'),
	("or",	'∨'),
	("ordf",	'ª'),
	("ordm",	'º'),
	("oslash",	'ø'),
	("otilde",	'õ'),
	("otimes",	'⊗'),
	("ouml",	'ö'),
	("para",	'¶'),
	("part",	'∂'),
	("permil",	'‰'),
	("perp",	'⊥'),
	("phi",	'φ'),
	("pi",	'π'),
	("piv",	'ϖ'),
	("plusmn",	'±'),
	("pound",	'£'),
	("prime",	'′'),
	("prod",	'∏'),
	("prop",	'∝'),
	("psi",	'ψ'),
	("quad", ' '),
	("quot",	'"'),
	("quot", '"'),
	("rArr",	'⇒'),
	("radic",	'√'),
	("rang",	'〉'),
	("raquo",	'»'),
	("rarr",	'→'),
	("rceil",	'⌉'),
	("rdquo",	'”'),
	("real",	'ℜ'),
	("reg",	'®'),
	("rfloor",	'⌋'),
	("rho",	'ρ'),
	("rlm",	'‏'),
	("rsaquo",	'›'),
	("rsquo",	'’'),
	("sbquo",	'‚'),
	("scaron",	'š'),
	("sdot",	'⋅'),
	("sect",	'§'),
	("shy",	'­'),
	("sigma",	'σ'),
	("sigmaf",	'ς'),
	("sim",	'∼'),
	("sp", ' '),
	("spades",	'♠'),
	("sub",	'⊂'),
	("sube",	'⊆'),
	("sum",	'∑'),
	("sup",	'⊃'),
	("sup1",	'¹'),
	("sup2",	'²'),
	("sup3",	'³'),
	("supe",	'⊇'),
	("szlig",	'ß'),
	("tau",	'τ'),
	("there4",	'∴'),
	("theta",	'θ'),
	("thetasym",	'ϑ'),
	("thinsp",	' '),
	("thorn",	'þ'),
	("tilde",	'˜'),
	("times",	'×'),
	("trade",	'™'),
	("uArr",	'⇑'),
	("uacute",	'ú'),
	("uarr",	'↑'),
	("ucirc",	'û'),
	("ugrave",	'ù'),
	("uml",	'¨'),
	("upsih",	'ϒ'),
	("upsilon",	'υ'),
	("uuml",	'ü'),
	("varepsilon", '∈'),
	("varphi", 'ϕ'),
	("varpi", 'ϖ'),
	("varrho", 'ϱ'),
	("vdots", '⋮'),
	("vsigma", 'ς'),
	("vtheta", 'ϑ'), 
	("weierp",	'℘'),
	("xi",	'ξ'),
	("yacute",	'ý'),
	("yen",	'¥'),
	("yuml",	'ÿ'),
	("zeta",	'ζ'),
	("zwj",	'‍'),
	("zwnj",	'‌'),
};

# Characters Winstart..Winend are those that Windows
# uses interpolated into the Latin1 set.
# They aren't supposed to appear in HTML, but they do....
Winstart : con 16r7f;
Winend: con 16r9f;
winchars := array[] of { '•',
	'•', '•', '‚', 'ƒ', '„', '…', '†', '‡',
	'ˆ', '‰', 'Š', '‹', 'Œ', '•', '•', '•',
	'•', '‘', '’', '“', '”', '•', '–', '—',
	'˜', '™', 'š', '›', 'œ', '•', '•', 'Ÿ' 
};

NAMCHAR : con (C->L|C->U|C->D|C->N);
LETTER : con (C->L|C->U);

dbg := 0;
warn := 0;

init(cu: CharonUtils)
{
	CU = cu;
	sys = load Sys Sys->PATH;
	S = load String String->PATH;
	C = cu->C;
	J = cu->J;
	T = load StringIntTab StringIntTab->PATH;
	tagtable = CU->makestrinttab(tagnames);
	attrtable = CU->makestrinttab(attrnames);
	ctype = C->ctype;
}

TokenSource.new(b: ref CU->ByteSource, chset : Btos, mtype: int) : ref TokenSource
{
	ts := ref TSstate (
		0,				# bi
		0, 				# prevbi
		"",				# s
		0,				# si
		Convcs->Startstate,	# state
		Convcs->Startstate	# prevstate
	);
	ans := ref TokenSource(
		b,			# b
		chset,		# chset
		ts,			# state
		mtype,		# mtype
		0			# inxmp
	);
	dbg = int (CU->config).dbg['x'];
	warn = (int (CU->config).dbg['w']) || dbg;
	return ans;
}

TokenSource.gettoks(ts: self ref TokenSource): array of ref Token
{
	ToksMax : con 500;		# max chunk of tokens returned
	a := array[ToksMax] of ref Token;
	ai := 0;
	pcdai := 0;
	lim := 0;
	# put some dbg output in here
	if(ts.mtype == CU->TextHtml) {
		pcdstate : ref TSstate;
gather:
		while(ai < ToksMax-1) {	# always allow space for a Data token
			state := getstate(ts);
			c := getchar(ts);
			if (c < ' ') {
				c = eatctls(c, ts);
				if (c < 0)
					break;
			}
			tok : ref Token;
			if(c == '<') {
				tok = gettag(ts);
				if (tok != nil && ts.inxmp && tok.tag != Txmp+RBRA) {
					rewind(ts, state);
					getchar(ts);	# consume the '<'
					tok = ref Token(Data, "<", nil);
				}
				if(tok != nil && tok.tag != Comment) {
					a[ai++] = tok;
					case (tok.tag) {
					Tselect or Ttitle or Toption=>
						# Several tags expect PCDATA after them.
						# Capture state so we can rewind if necessary
						pcdstate = state;
						pcdai = ai-1;
					Ttextarea =>
						pcdstate = state;
						pcdai = ai-1;
						# not sure if we should parse entity references
						tok = gettagdata(ts, tok.tag, 1);
						if(tok != nil) {
							pcdstate = nil;
							a[ai++] = tok;
						}
					Tscript =>
						pcdstate = state;
						pcdai = ai-1;
						# special rules for getting Data
						tok = getscriptdata(ts);
						if(tok != nil) {
							pcdstate = nil;
							a[ai++] = tok;
						}
					Txmp =>
						pcdstate = nil;
						ts.inxmp = 1;
					Txmp+RBRA =>
						pcdstate = nil;
						ts.inxmp = 0;
					Data =>
						;
					Tmeta =>
						pcdstate = nil;
						break gather;
					* =>
						pcdstate = nil;
					}
				}
			} else {
				tok = getdata(ts, c);
				if(tok != nil)
					a[ai++] = tok;
			}
			if(tok == nil && !eof(ts)) {
				# we need more input to complete the token
				lim = ts.state.bi;
				rewind(ts, state);
				break gather;
			} else
				if(dbg > 1)
					sys->print("lex: got token %s\n", tok.tostring());
		}
		# Several tags expect PCDATA after them.
		# which means that build needs to see another tag or eof
		# after any data in order to know that PCDATA is ended.
		# Rewind if we haven't got to the following tag yet.
		if (pcdstate != nil && !eof(ts)) {
			rewind(ts, pcdstate);
			ai = pcdai;
		}
	}
	else {
		# plain text (non-html) tokens
		while(ai < ToksMax) {
			tok := getplaindata(ts);
			if(tok == nil)
				break;
			else
				a[ai++] = tok;
			if(dbg > 1)
				sys->print("lex: got token %s\n", tok.tostring());
		}
	}
	if(dbg)
		sys->print("lex: returning %d tokens\n", ai);
	if (lim > ts.b.lim)
		ts.b.lim = lim;
	else
		ts.b.lim = ts.state.prevbi;
	if(ai == 0)
		return nil;
	return a[0:ai];
}

# must not be called from within TokenSource.gettoks()
# as it will not work with rewind() and ungetchar()
#
TokenSource.setchset(ts: self ref TokenSource, chset: Btos)
{
	st := ts.state;
	nchars := st.si;
	if (nchars > 0 && nchars < len st.s) {
		# align bi to the current input char
		bs := ts.b;
		(nil, nil, n) := ts.chset->btos(st.prevcsstate, bs.data[st.prevbi:st.bi], nchars);
		st.bi = st.prevbi + n;
		st.prevbi = st.bi;
	}
	ts.chset = chset;
	st.csstate = st.prevcsstate = Convcs->Startstate;
	st.s = nil;
	st.si = 0;
}


eof(ts : ref TokenSource) : int
{
	st := ts.state;
	bs := ts.b;
	return (st.s == nil && bs.eof && st.prevbi == bs.edata);
}

# For case where source isn't HTML.
# Just make data tokens, one per line (or partial line,
# at end of buffer), ignoring non-whitespace control
# characters and dumping \r's
getplaindata(ts: ref TokenSource): ref Token
{
	s := "";
	j := 0;

	for(c := getchar(ts); c >= 0; c = getchar(ts)) {
		if(c < ' ') {
			if(ctype[c] == C->W) {
				if(c == '\r') {
					# ignore it unless no following '\n',
					# in which case treat it like '\n'
					c = getchar(ts);
					if(c != '\n') {
						if(c >= 0)
							ungetchar(ts);
						c = '\n';
					}
				}
			}
			else
				c = 0;	# ignore
		}
		if(c != 0)
			s[j++] = c;
		if(c == '\n')
			break;
	}
	if(s == "")
		return nil;
	return ref Token(Data, s, nil);
}

eatctls(c: int, ts: ref TokenSource): int
{
	while (c >= 0) {
		if (c >= ' ')
			return c;
		if(ctype[c] == C->W) {
			if(c == '\r') {
				c = getchar(ts);
				if (c != '\n' && c >= 0) {
					ungetchar(ts);
					c = '\n';
				}
			}
			return c;
		}
		c = getchar(ts);
	}
	return -1;
}

# Gather data up to next start-of-tag or end-of-buffer.
# Translate entity references (&amp;) if not in <XMP> section.
# Ignore non-whitespace control characters and get rid of \r's.
getdata(ts: ref TokenSource, firstc : int): ref Token
{
	s := "";
	j := 0;
	c := firstc;

	while(c >= 0) {
		if (c < ' ')
			c = eatctls(c, ts);
		if (c < 0)
			break;
		if(c == '&' && !ts.inxmp) {
			ok : int;
			(c, ok) = ampersand(ts);
			if(!ok) {
				ungetchar(ts);
				break;	# incomplete entity reference (ts backed up by ampersand)
			}
		}
		else if(c == '<') {
			ungetchar(ts);
			break;
		}
		if(c != 0)
			s[j++] = c;
		c = getchar(ts);
	}
	if(s == "")
		return nil;
	return ref Token(Data, s, nil);
}

# The rules for lexing scripts are different (ugh).
# Gather up everything until see a </SCRIPT>.
getscriptdata(ts: ref TokenSource): ref Token
{
	tok := gettagdata(ts, Tscript, 0);
	if (tok != nil)
		tok.text = CU->stripscript(tok.text);
	return tok;
}

gettagdata(ts: ref TokenSource, tag, doentities: int): ref Token
{
	s := "";
	j := 0;
	c := getchar(ts);

	while(c >= 0) {
		if (c == '<') {
			tstate := getstate(ts);
			tok := gettag(ts);
			rewind(ts, tstate);
			if (tok != nil && tok.tag == tag+RBRA) {
				ungetchar(ts);
				return ref Token(Data, s, nil);
			}
			# tag was not </tag>, take as regular data
		}
		if (doentities && c == '&')
			(c, nil) = ampersand(ts);

		if(c < 0)
			break;
		if(c != 0)
			s[j++] = c;
		c = getchar(ts);
	}
	if(eof(ts))
		return ref Token(Data, s, nil);

	return nil;
}

# We've just seen a '<'.  Gather up stuff to closing '>' (if buffer
# ends before then, return nil).
# If it's a tag, look up the name, gather the attributes, and return
# the appropriate token.
# Else it's either just plain data or some kind of ignorable stuff:
# return a Data or Comment token as appropriate.
gettag(ts: ref TokenSource): ref Token
{
	rbra := 0;
	ans : ref Token = nil;
	al: list of Attr;
	start := getstate(ts);
	c := getchar(ts);

	# dummy loop: break out of this when hit end of buffer
 eob:
	for(;;) {
		if(c == '/') {
			rbra = RBRA;
			c = getchar(ts);
		}
		if(c < 0)
			break eob;
		if(c>=C->NCTYPE || !int (ctype[c]&LETTER)) {
			# not a tag
			if(c == '!') {
				ans = comment(ts);
				if(ans != nil)
					return ans;
				break eob;
			}
			else {
				rewind(ts, start);
				return ref Token(Data, "<", nil);
			}
		}
		# c starts a tagname
		ans = ref Token(Notfound, nil, nil);
		name := "";
		name[0] = lowerc(c);
		i := 1;
		for(;;) {
			c = getchar(ts);
			if(c < 0)
				break eob;
			if(c>=C->NCTYPE || !int (ctype[c]&NAMCHAR))
				break;
			name[i++] = lowerc(c);
		}
		(fnd, tag) := T->lookup(tagtable, name);
		if(fnd)
			ans.tag = tag+rbra;
		else
			ans.text = name;	# for warning print, in build
attrloop:
		for(;;) {
			# look for "ws name" or "ws name ws = ws val"  (ws=whitespace)
			# skip whitespace
			while(c < C->NCTYPE && ctype[c] == C->W) {
				c = getchar(ts);
				if(c < 0)
					break eob;
			}
			if(c == '>')
				break attrloop;
			if(c == '<') {
				if(warn)
					sys->print("warning: unclosed tag; last name=%s\n", name);
				ungetchar(ts);
				break attrloop;
			}
			if(c >= C->NCTYPE || !int (ctype[c]&LETTER)) {
				if(warn)
					sys->print("warning: expected attribute name; last name=%s\n", name);
				# skip to next attribute name
				for(;;) {
					c = getchar(ts);
					if(c < 0)
						break eob;
					if(c < C->NCTYPE && int (ctype[c]&LETTER))
						continue attrloop;
					if(c == '<') {
						if(warn)
							sys->print("warning: unclosed tag; last name=%s\n", name);
						ungetchar(ts);
						break attrloop;
					}
					if(c == '>')
						break attrloop;
				}
			}
			# gather attribute name
			name = "";
			name[0] = lowerc(c);
			i = 1;
			for(;;) {
				c = getchar(ts);
				if(c < 0)
					break eob;
				if(c >= C->NCTYPE || !int (ctype[c]&NAMCHAR))
					break;
				name[i++] = lowerc(c);
			}
			(afnd, attid) := T->lookup(attrtable, name);
			if(warn && !afnd)
				sys->print("warning: unknown attribute name %s\n", name);
			# skip whitespace
			while(c < C->NCTYPE && ctype[c] == C->W) {
				c = getchar(ts);
				if(c < 0)
					break eob;
			}
			if(c != '=') {
				# no value for this attr
				if(afnd)
					al = (attid, "") :: al;
				continue attrloop;
			}
			# c is '=' here;  skip whitespace
			for(;;) {
				c = getchar(ts);
				if(c < 0)
					break eob;
				if(c >= C->NCTYPE || ctype[c] != C->W)
					break;
			}
			# gather value
			quote := 0;
			if(c == '\'' || c == '"') {
				quote = c;
				c = getchar(ts);
				if(c < 0)
					break eob;
			}
			val := "";
			nv := 0;
		valloop:
			for(;;) {
				if(c < 0)
					break eob;
# other browsers allow value strings to be broken across lines
# especially the case for Javascript event handlers / URLs
				if (c == '>' && !quote)
					break valloop;
# old code otherwise ok - keep for now for reference
#				if(c == '>') {
#					if(quote) {
#						# c might be part of string (though not good style)
#						# but if line ends before close quote, assume
#						# there was an unmatched quote
#						ti := ts.i;
#						for(;;) {
#							c = getchar(ts);
#							if(c < 0)
#								break eob;
#							if(c == quote) {
#								backup(ts, ti);
#								val[nv++] = '>';
#								c = getchar(ts);
#								continue valloop;
#							}
#							if(c == '\n') {
#								if(warn)
#									sys->print("warning: apparent unmatched quote\n");
#								backup(ts, ti);
#								quote = 0;
#								c = '>';
#								break valloop;
#							}
#						}
#					}
#					else
#						break valloop;
#				}
				if(quote) {
					if(c == quote) {
						c = getchar(ts);
						if(c < 0)
							break eob;
						break valloop;
					}
					if(c == '\r') {
						c = getchar(ts);
						continue valloop;
					}
					if(c == '\t' || c == '\n')
						c = ' ';
				}
				else {
					if(c < C->NCTYPE && ctype[c]==C->W)
						break valloop;
				}
				if(c == '&') {
					ok : int;
					(c, ok) = ampersand(ts);
					if(!ok)
						break eob;
				}
				val[nv++] = c;
				c = getchar(ts);
			}
			if(afnd)
				al = (attid, val) :: al;
		}
		ans.attr = al;
		return ans;
	}
	if(eof(ts)) {
		if(warn)
			sys->print("warning: incomplete tag at end of page\n");
		rewind(ts, start);
		return ref Token(Data, "<", nil);
	}
	return nil;
}


# We've just read a '<!',
# so this may be a comment or other ignored section, or it may
# be just a literal string if there is no close before end of file
# (other browsers do that).
# The accepted practice seems to be (note: contrary to SGML spec!):
# If see <!--, look for --> to close, or if none, > to close.
# If see <!(not --), look for > to close.
# If no close before end of file, leave original characters in as literal data.
#
# If we see ignorable stuff, return Comment token.
# Else return nil (caller should back up and try again when more data arrives,
# unless at end of file, in which case caller should just make '<' a data token).
comment(ts: ref TokenSource) : ref Token
{
	havecomment := 0;
	commentstart := 0;
	c := getchar(ts);
	if(c == '-') {
		state := getstate(ts);
		c = getchar(ts);
		if(c == '-') {
			commentstart = 1;
			if(findstr(ts, "-->"))
				havecomment = 1;
			else
				rewind(ts, state);
		}
	}
	if(!havecomment) {
		if(c == '>')
			havecomment = 1;
		else if(c >= 0) {
			if(findstr(ts, ">"))
				havecomment = 1;
		}
	}
	if(havecomment)
		return ref Token(Comment, nil, nil);
	return nil;
}

# Look for string s in token source.
# If found, return 1, with buffer at next char after s,
# else return 0 (caller should back up).
findstr(ts: ref TokenSource, s: string) : int
{
	n := len s;
	eix := n-1;
	buf := "";
	c : int;

	if (n == 1) {
		while ((c = getchar(ts)) >= 0)
			if (c == s[0])
				return 1;
		return 0;
	}

	for (i := 0; i < n; i++) {
		c = getchar(ts);
		if (c < 0)
			return 0;
		buf[i] = c;
	}

	for (;;) {
		# this could be much more efficient by tracking
		# the start char through buf
		if (buf == s)
			return 1;
		c = getchar(ts);
		if (c < 0)
			return 0;
		buf = buf[1:];
		buf[eix] = c;
	}
	return 0;	# keep the compiler quiet
}

# We've just read an '&'; look for an entity reference
# name, and if found, return (translated char, 1).
# Otherwise the input stream is rewound to just after
# the '&'
# if there is a complete entity name but it isn't known,
# ('&', 1) is returned, if an incomplete name is encountered
# (0, 0) is returned
ampersand(ts: ref TokenSource): (int, int)
{
	state := getstate(ts);
	c := getchar(ts);
	fnd := 0;
	ans := 0;
	if(c == '#') {
		v := 0;
		c = getchar(ts);
		if (c == 'x' || c == 'X') {
			for (c = getchar(ts); c >= 0; c = getchar(ts)) {
				if (int (ctype[c] & C->D)) {
					v = v*16 + c-'0';
					continue;
				}
				c = lowerc(c);
				if (c >= 'a' && c <= 'f') {
					v = v*16 + 10 + c-'a';
					continue;
				}
				break;
			}
		} else {
			while(c >= 0) {
				if(ctype[c] != C->D)
					break;
				v = v*10 + c-'0';
				c = getchar(ts);
			}
		}
		if(c >= 0) {
			if(!(c == ';' || c == '\n' || c == '\r' || c == '<'))
				ungetchar(ts);
			c = v;
			if(c==160)
				c = ' ';   # non-breaking space
			if(c >= Winstart && c <= Winend)
				c = winchars[c-Winstart];
			ans = c;
			fnd = (v != 0);
		}
	}
	# only US-ASCII chars can make up &charnames;
	else if(c >= 0 && c < 16r80 && int (ctype[c] & LETTER)) {
		s := "";
		s[0] = c;
		k := 1;
		for(;;) {
			c = getchar(ts);
			if(c < 0)
				break;
			if(c < 16r80 && int (ctype[c]&NAMCHAR))
				s[k++] = c;
			else {
				if(!(c == ';' || c == '\n' || c == '\r'))
					ungetchar(ts);
				break;
			}
		}
		if (c < 0 || c == ' ' || c == ';' || c == '\n' || c == '\r' || c == '<')
			(fnd, ans) = T->lookup(chartab, s);
	}
	if(!fnd) {
		if(c < 0 && !eof(ts)) {
			# was incomplete
			rewind(ts, state);
			return (0, 0);
		}
		else {
			rewind(ts, state);
			return ('&', 1);
		}
	}
	# elide soft hyphens (&shy; / &xAD;)
# not suficient - need to do it for all input in getdata() which is too heavy handed
#	if (ans == '­')
#		ans = 0;
	return (ans, 1);
}

# If c is an uppercase letter, return its lowercase version,
# otherwise return c.
# Assume c is a NAMCHAR, so don't need range check on ctype[]
lowerc(c: int) : int
{
	if(ctype[c] == C->U) {
		# this works for accented characters in Latin1, too
		return c + 16r20;
	}
	return c;
}

Token.aval(t: self ref Token, attid: int): (int, string)
{
	attr := t.attr;
	while(attr != nil) {
		a := hd attr;
		if(a.attid == attid)
			return (1, a.value); 
		attr = tl attr;
	}
	return (0, "");
}


# for debugging
Token.tostring(t: self ref Token) : string
{
	ans := "";
	tag := t.tag;
	if(tag == Data)
		ans = ans + "'" + t.text + "'";
	else {
		ans = ans + "<";
		if(tag >= RBRA) {
			tag -= RBRA;
			ans = ans + "/";
		}
		tname := tagnames[tag];
		if(tag == Notfound)
			tname = "?";
		ans = ans + S->toupper(tname);
		for(al := t.attr; al != nil; al = tl al) {
			a := hd al;
			aname := attrnames[a.attid];
			ans = ans + " " + aname;
			if(a.value != "")
				ans = ans + "='" + a.value + "'";
		}
		ans = ans + ">";
	}
	return ans;
}


CONVBLK : con 1024;		# number of characters to convert at a time

# Returns -1 if no complete character left before current end of data.
getchar(ts: ref TokenSource): int
{
	st := ts.state;
	if (st.s == nil || st.si >= len st.s) {
		bs := ts.b;
		st.si = 0;
		st.s = "";
		st.prevcsstate = st.csstate;
		st.prevbi = st.bi;
		edata := bs.edata;
		if (st.bi >= edata)
			return -1;
		(state, s, n ) := ts.chset->btos(st.csstate, bs.data[st.bi:edata], CONVBLK);
		if (s == nil) {
			if (bs.eof && edata == bs.edata) {
				# must have been an encoding error at eof
				st.prevbi = st.bi = edata;
			}
			return -1;
		}
		st.csstate = state;
		st.s = s;
		st.bi += n;
	}
	return st.s[st.si++];
}

# back up by one input character
# NOTE: can only call this function post a successful getchar() call
ungetchar(ts : ref TokenSource)
{
	st := ts.state;
	# assert(len st.s >= 1 && st.si > 0)
	if (st.si <= 0)
		raise "EXInternal:too many backups";
	st.si--;
}

rewind(ts : ref TokenSource, state : ref TSstate)
{
	ts.state = state;
}

# return a copy of the TokenSource state
getstate(ts : ref TokenSource) : ref TSstate
{
	return ref *ts.state;
}