xref: /inferno-os/appl/cmd/man2html.b (revision 7cdb1d14cab5ad4eceb9edfc484ea272cf8a062d)
1implement Man2html;
2
3include "sys.m";
4	stderr: ref Sys->FD;
5	sys: Sys;
6	print, fprint, sprint: import sys;
7
8
9include "bufio.m";
10
11include "draw.m";
12
13include "daytime.m";
14	dt: Daytime;
15
16include "string.m";
17	str: String;
18
19include "arg.m";
20
21Man2html: module
22{
23	init:	fn(ctxt: ref Draw->Context, args: list of string);
24};
25
26Runeself: con 16r80;
27false, true: con iota;
28
29Troffspec: adt {
30	name: string;
31	value: string;
32};
33
34tspec := array [] of { Troffspec
35	("ff", "ff"),
36	("fi", "fi"),
37	("fl", "fl"),
38	("Fi", "ffi"),
39	("ru", "_"),
40	("em", "—"),
41	("14", "¼"),
42	("12", "½"),
43	("co", "©"),
44	("de", "°"),
45	("dg", "¡"),
46	("fm", "´"),
47	("rg", "®"),
48#	("bu", "*"),
49	("bu", "•"),
50	("sq", "¤"),
51	("hy", "-"),
52	("pl", "+"),
53	("mi", "-"),
54	("mu", "×"),
55	("di", "÷"),
56	("eq", "="),
57	("==", "=="),
58	(">=", ">="),
59	("<=", "<="),
60	("!=", "!="),
61	("+-", "&#177;"),
62	("no", "&#172;"),
63	("sl", "/"),
64	("ap", "&"),
65	("~=", "~="),
66	("pt", "oc"),
67	("gr", "GRAD"),
68	("->", "->"),
69	("<-", "<-"),
70	("ua", "^"),
71	("da", "v"),
72	("is", "Integral"),
73	("pd", "DIV"),
74	("if", "oo"),
75	("sr", "-/"),
76	("sb", "(~"),
77	("sp", "~)"),
78	("cu", "U"),
79	("ca", "(^)"),
80	("ib", "(="),
81	("ip", "=)"),
82	("mo", "C"),
83	("es", "&Oslash;"),
84	("aa", "&#180;"),
85	("ga", "`"),
86	("ci", "O"),
87	("L1", "Lucent"),
88	("sc", "&#167;"),
89	("dd", "++"),
90	("lh", "<="),
91	("rh", "=>"),
92	("lt", "("),
93	("rt", ")"),
94	("lc", "|"),
95	("rc", "|"),
96	("lb", "("),
97	("rb", ")"),
98	("lf", "|"),
99	("rf", "|"),
100	("lk", "|"),
101	("rk", "|"),
102	("bv", "|"),
103	("ts", "s"),
104	("br", "|"),
105	("or", "|"),
106	("ul", "_"),
107	("rn", " "),
108	("*p", "PI"),
109	("**", "*"),
110};
111
112	Entity: adt {
113		 name: string;
114		 value: int;
115	};
116	Entities: array of Entity;
117
118Entities = array[] of {
119		Entity( "&#161;",	'¡' ),
120		Entity( "&#162;",	'¢' ),
121		Entity( "&#163;",	'£' ),
122		Entity( "&#164;",	'¤' ),
123		Entity( "&#165;",	'¥' ),
124		Entity( "&#166;",	'¦' ),
125		Entity( "&#167;",	'§' ),
126		Entity( "&#168;",	'¨' ),
127		Entity( "&#169;",	'©' ),
128		Entity( "&#170;",	'ª' ),
129		Entity( "&#171;",	'«' ),
130		Entity( "&#172;",	'¬' ),
131		Entity( "&#173;",	'­' ),
132		Entity( "&#174;",	'®' ),
133		Entity( "&#175;",	'¯' ),
134		Entity( "&#176;",	'°' ),
135		Entity( "&#177;",	'±' ),
136		Entity( "&#178;",	'²' ),
137		Entity( "&#179;",	'³' ),
138		Entity( "&#180;",	'´' ),
139		Entity( "&#181;",	'µ' ),
140		Entity( "&#182;",	'¶' ),
141		Entity( "&#183;",	'·' ),
142		Entity( "&#184;",	'¸' ),
143		Entity( "&#185;",	'¹' ),
144		Entity( "&#186;",	'º' ),
145		Entity( "&#187;",	'»' ),
146		Entity( "&#188;",	'¼' ),
147		Entity( "&#189;",	'½' ),
148		Entity( "&#190;",	'¾' ),
149		Entity( "&#191;",	'¿' ),
150		Entity( "&Agrave;",	'À' ),
151		Entity( "&Aacute;",	'Á' ),
152		Entity( "&Acirc;",	'Â' ),
153		Entity( "&Atilde;",	'Ã' ),
154		Entity( "&Auml;",	'Ä' ),
155		Entity( "&Aring;",	'Å' ),
156		Entity( "&AElig;",	'Æ' ),
157		Entity( "&Ccedil;",	'Ç' ),
158		Entity( "&Egrave;",	'È' ),
159		Entity( "&Eacute;",	'É' ),
160		Entity( "&Ecirc;",	'Ê' ),
161		Entity( "&Euml;",	'Ë' ),
162		Entity( "&Igrave;",	'Ì' ),
163		Entity( "&Iacute;",	'Í' ),
164		Entity( "&Icirc;",	'Î' ),
165		Entity( "&Iuml;",	'Ï' ),
166		Entity( "&ETH;",	'Ð' ),
167		Entity( "&Ntilde;",	'Ñ' ),
168		Entity( "&Ograve;",	'Ò' ),
169		Entity( "&Oacute;",	'Ó' ),
170		Entity( "&Ocirc;",	'Ô' ),
171		Entity( "&Otilde;",	'Õ' ),
172		Entity( "&Ouml;",	'Ö' ),
173		Entity( "&215;",	'×' ),
174		Entity( "&Oslash;",	'Ø' ),
175		Entity( "&Ugrave;",	'Ù' ),
176		Entity( "&Uacute;",	'Ú' ),
177		Entity( "&Ucirc;",	'Û' ),
178		Entity( "&Uuml;",	'Ü' ),
179		Entity( "&Yacute;",	'Ý' ),
180		Entity( "&THORN;",	'Þ' ),
181		Entity( "&szlig;",	'ß' ),
182		Entity( "&agrave;",	'à' ),
183		Entity( "&aacute;",	'á' ),
184		Entity( "&acirc;",	'â' ),
185		Entity( "&atilde;",	'ã' ),
186		Entity( "&auml;",	'ä' ),
187		Entity( "&aring;",	'å' ),
188		Entity( "&aelig;",	'æ' ),
189		Entity( "&ccedil;",	'ç' ),
190		Entity( "&egrave;",	'è' ),
191		Entity( "&eacute;",	'é' ),
192		Entity( "&ecirc;",	'ê' ),
193		Entity( "&euml;",	'ë' ),
194		Entity( "&igrave;",	'ì' ),
195		Entity( "&iacute;",	'í' ),
196		Entity( "&icirc;",	'î' ),
197		Entity( "&iuml;",	'ï' ),
198		Entity( "&eth;",	'ð' ),
199		Entity( "&ntilde;",	'ñ' ),
200		Entity( "&ograve;",	'ò' ),
201		Entity( "&oacute;",	'ó' ),
202		Entity( "&ocirc;",	'ô' ),
203		Entity( "&otilde;",	'õ' ),
204		Entity( "&ouml;",	'ö' ),
205		Entity( "&247;",	'÷' ),
206		Entity( "&oslash;",	'ø' ),
207		Entity( "&ugrave;",	'ù' ),
208		Entity( "&uacute;",	'ú' ),
209		Entity( "&ucirc;",	'û' ),
210		Entity( "&uuml;",	'ü' ),
211		Entity( "&yacute;",	'ý' ),
212		Entity( "&thorn;",	'þ' ),
213		Entity( "&yuml;",	'ÿ' ),		# &#255;
214
215		Entity( "&#SPACE;",	' ' ),
216		Entity( "&#RS;",	'\n' ),
217		Entity( "&#RE;",	'\r' ),
218		Entity( "&quot;",	'"' ),
219		Entity( "&amp;",	'&' ),
220		Entity( "&lt;",	'<' ),
221		Entity( "&gt;",	'>' ),
222
223		Entity( "CAP-DELTA",	'Δ' ),
224		Entity( "ALPHA",	'α' ),
225		Entity( "BETA",	'β' ),
226		Entity( "DELTA",	'δ' ),
227		Entity( "EPSILON",	'ε' ),
228		Entity( "THETA",	'θ' ),
229		Entity( "MU",		'μ' ),
230		Entity( "PI",		'π' ),
231		Entity( "TAU",	'τ' ),
232		Entity( "CHI",	'χ' ),
233
234		Entity( "<-",		'←' ),
235		Entity( "^",		'↑' ),
236		Entity( "->",		'→' ),
237		Entity( "v",		'↓' ),
238		Entity( "!=",		'≠' ),
239		Entity( "<=",		'≤' ),
240		Entity( nil, 0 ),
241};
242
243
244Hit: adt {
245	glob: string;
246	chap: string;
247	mtype: string;
248	page: string;
249};
250
251Lnone, Lordered, Lunordered, Ldef, Lother: con iota;	# list types
252
253Chaps: adt {
254	name: string;
255	primary: int;
256};
257
258Types: adt {
259	name: string;
260	desc: string;
261};
262
263
264# having two separate flags here allows for inclusion of old-style formatted pages
265# under a new-style three-level tree
266Oldstyle: adt {
267	names: int;	# two-level directory tree?
268	fmt: int;		# old internal formats: e.g., "B" font means "L"; name in .TH in all caps
269};
270
271Href: adt {
272	title: string;
273	chap: string;
274	mtype: string;
275	man: string;
276};
277
278# per-thread global data
279Global: adt {
280	bufio: Bufio;
281	bin: ref Bufio->Iobuf;
282	bout: ref Bufio->Iobuf;
283	topname: string;		# name of the top level categories in the manual
284	chaps: array of Chaps;	# names of top-level partitions of this manual
285	types: array of Types;	# names of second-level partitions
286	oldstyle: Oldstyle;
287	mantitle: string;
288	mandir: string;
289	thisone: Hit;		# man page we're displaying
290	mtime: int;			# last modification time of thisone
291	href: Href;			# hrefs of components of this man page
292	hits: array of Hit;
293	nhits: int;
294	list_type: int;
295	pm: string;			# proprietary marking
296	def_goobie: string;	# deferred goobie
297	sop: int;			# output at start of paragraph?
298	sol: int;			# input at start of line?
299	broken: int;		# output at a break?
300	fill: int;			# in fill mode?
301 	pre: int;			# in PRE block?
302	example: int;		# an example active?
303	ipd: int;			# emit inter-paragraph distance?
304	indents: int;
305	hangingdt: int;
306	curfont: string;		# current font
307	prevfont: string;		# previous font
308	lastc: int;			# previous char from input scanner
309	def_sm: int;		# amount of deferred "make smaller" request
310
311	mk_href_chap: fn(g: self ref Global, chap: string);
312	mk_href_man: fn(g: self ref Global, man: string, oldstyle: int);
313	mk_href_mtype: fn(g: self ref Global, chap, mtype: string);
314	dobreak: fn(g: self ref Global);
315	print: fn(g: self ref Global, s: string);
316	softbr: fn(g: self ref Global): string;
317	softp: fn(g: self ref Global): string;
318};
319
320header := "<HTML><HEAD>";
321initial := "";
322trailer := "</BODY></HTML>";
323
324usage()
325{
326	sys->fprint(stderr, "Usage: man2html [-h header] [-i initialtext] [-t trailer] file [section]\n");
327	raise "fail:usage";
328}
329
330
331init(nil: ref Draw->Context, args: list of string)
332{
333	sys = load Sys Sys->PATH;
334	stderr = sys->fildes(2);
335	str = load String String->PATH;
336	dt = load Daytime Daytime->PATH;
337	arg := load Arg Arg->PATH;
338	arg->init(args);
339	arg->setusage("man2html [-h header] [-t trailer] file [section]");
340	while((o := arg->opt()) != 0)
341		case o {
342		'h' =>	header = arg->earg();
343		't' =>	trailer = arg->earg();
344		* =>	arg->usage();
345		}
346	args = arg->argv();
347	if(args == nil)
348		arg->usage();
349	arg = nil;
350	g := Global_init();
351	page := hd args;
352	args = tl args;
353	section := "1";
354	if(args != nil)
355		section = hd args;
356	hit := Hit ("", "man", section, page);
357	domanpage(g, hit);
358	g.print(trailer+"\n");
359	g.bufio->g.bout.flush();
360}
361
362# remove markup from a string
363# doesn't handle nested/quoted delimiters
364demark(s: string): string
365{
366	t: string;
367	clean := true;
368	for (i := 0; i < len s; i++) {
369		case s[i] {
370		'<' =>
371			clean = false;
372		'>' =>
373			clean = true;
374		* =>
375			if (clean)
376				t[len t] = s[i];
377		}
378	}
379	return t;
380}
381
382
383#
384#  Convert an individual man page to HTML and output.
385#
386domanpage(g: ref Global, man: Hit)
387{
388	file := man.page;
389	g.bin = g.bufio->open(file, Bufio->OREAD);
390	g.bout = g.bufio->fopen(sys->fildes(1), Bufio->OWRITE);
391	if (g.bin == nil) {
392		fprint(stderr, "Cannot open %s: %r\n", file);
393		return;
394	}
395	(err, info) := sys->fstat(g.bin.fd);
396	if (! err) {
397		g.mtime = info.mtime;
398	}
399	g.thisone = man;
400	while ((p := getnext(g)) != nil) {
401		c := p[0];
402		if (c == '.' && g.sol) {
403			if (g.pre) {
404				g.print("</PRE>");
405				g.pre = false;
406			}
407			dogoobie(g, false);
408			dohangingdt(g);
409		} else if (g.def_goobie != nil || g.def_sm != 0) {
410			g.bufio->g.bin.ungetc();
411			dogoobie(g, true);
412		} else if (c == '\n') {
413			g.print(p);
414			dohangingdt(g);
415		} else
416			g.print(p);
417	}
418	if (g.pm != nil) {
419		g.print("<BR><BR><BR><FONT SIZE=-2><CENTER>\n");
420		g.print(g.pm);
421		g.print("<BR></CENTER></FONT>\n");
422	}
423	closeall(g, 0);
424	rev(g, g.bin);
425}
426
427dogoobie(g: ref Global, deferred: int)
428{
429	# read line, translate special chars
430	line := getline(g);
431	if (line == nil || line == "\n")
432		return;
433
434	# parse into arguments
435	token: string;
436	argl, rargl: list of string;	# create reversed version, then invert
437	while ((line = str->drop(line, " \t\n")) != nil)
438		if (line[0] == '"') {
439			(token, line) = split(line[1:], '"');
440			rargl = token :: rargl;
441		} else {
442			(token, line) = str->splitl(line, " \t");
443			rargl = token :: rargl;
444		}
445
446	if (rargl == nil && !deferred)
447		return;
448	for ( ; rargl != nil; rargl = tl rargl)
449		argl = hd rargl :: argl;
450
451	def_sm := g.def_sm;
452	if (deferred && def_sm > 0) {
453		g.print(sprint("<FONT SIZE=-%d>", def_sm));
454		if (g.def_goobie == nil)
455			argl = "dS" :: argl;	# dS is our own local creation
456	}
457
458	subgoobie(g, argl);
459
460	if (deferred && def_sm > 0) {
461		g.def_sm = 0;
462		g.print("</FONT>");
463	}
464}
465
466subgoobie(g: ref Global, argl: list of string)
467{
468	if (g.def_goobie != nil) {
469		argl = g.def_goobie :: argl;
470		g.def_goobie = nil;
471		if (tl argl == nil)
472			return;
473	}
474
475	# the command part is at most two characters, but may be concatenated with the first arg
476	cmd := hd argl;
477	argl = tl argl;
478	if (len cmd > 2) {
479		cmd = cmd[0:2];
480		argl =  cmd[2:] :: argl;
481	}
482
483	case cmd {
484
485	"B" or "I" or "L" or "R" =>
486		font(g, cmd, argl);		# "R" macro implicitly generated by deferred R* macros
487
488	"BI" or "BL" or "BR" or
489	"IB" or "IL" or
490	"LB" or "LI" or
491	"RB" or "RI" or "RL" =>
492		altfont(g, cmd[0:1], cmd[1:2], argl, true);
493
494	"IR" or "LR" =>
495		anchor(g, cmd[0:1], cmd[1:2], argl);		# includes man page refs ("IR" is old style, "LR" is new)
496
497	"dS" =>
498		printargs(g, argl);
499		g.print("\n");
500
501	"1C" or "2C" or "DT" or "TF" =>	 # ignore these
502		return;
503
504	"ig" =>
505		while ((line := getline(g)) != nil){
506			if(len line > 1 && line[0:2] == "..")
507				break;
508		}
509		return;
510
511	"P" or "PP" or "LP" =>
512			g_PP(g);
513
514	"EE" =>	g_EE(g);
515	"EX" =>	g_EX(g);
516	"HP" =>	g_HP_TP(g, 1);
517	"IP" =>	g_IP(g, argl);
518	"PD" =>	g_PD(g, argl);
519	"PM" =>	g_PM(g, argl);
520	"RE" =>	g_RE(g);
521	"RS" =>	g_RS(g);
522	"SH" =>	g_SH(g, argl);
523	"SM" =>	g_SM(g, argl);
524	"SS" =>	g_SS(g, argl);
525	"TH" =>	g_TH(g, argl);
526	"TP" =>	g_HP_TP(g, 3);
527
528	"br" =>	g_br(g);
529	"sp" =>	g_sp(g, argl);
530	"ti" =>	g_br(g);
531	"nf" =>	g_nf(g);
532	"fi" =>	g_fi(g);
533	"ft" =>	g_ft(g, argl);
534
535	* =>		return;		# ignore unrecognized commands
536	}
537
538}
539
540g_br(g: ref Global)
541{
542	if (g.hangingdt != 0) {
543		g.print("<DD>");
544		g.hangingdt = 0;
545	} else if (g.fill && ! g.broken)
546		g.print("<BR>\n");
547	g.broken = true;
548}
549
550g_EE(g: ref Global)
551{
552	g.print("</PRE>\n");
553	g.fill = true;
554	g.broken = true;
555	g.example = false;
556}
557
558g_EX(g: ref Global)
559{
560	g.print("<PRE>");
561	if (! g.broken)
562		g.print("\n");
563	g.sop = true;
564	g.fill = false;
565	g.broken = true;
566	g.example = true;
567}
568
569g_fi(g: ref Global)
570{
571	if (g.fill)
572		return;
573	g.fill = true;
574	g.print("<P style=\"display: inline; white-space: normal\">\n");
575	g.broken = true;
576	g.sop = true;
577}
578
579g_ft(g: ref Global, argl: list of string)
580{
581	font: string;
582	arg: string;
583
584	if (argl == nil)
585		arg = "P";
586	else
587		arg = hd argl;
588
589	if (g.curfont != nil)
590		g.print(sprint("</%s>", g.curfont));
591
592	case arg {
593	"2" or "I" =>
594		font = "I";
595	"3" or "B" =>
596		font = "B";
597	"5" or "L" =>
598		font = "TT";
599	"P" =>
600		font = g.prevfont;
601	* =>
602		font = nil;
603	}
604	g.prevfont = g.curfont;
605	g.curfont = font;
606	if (g.curfont != nil)
607		if (g.fill)
608			g.print(sprint("<%s>", g.curfont));
609		else
610			g.print(sprint("<%s style=\"white-space: pre\">", g.curfont));
611}
612
613# level == 1 is a .HP; level == 3 is a .TP
614g_HP_TP(g: ref Global, level: int)
615{
616	case g.list_type {
617	Ldef =>
618		if (g.hangingdt != 0)
619			g.print("<DD>");
620		g.print(g.softbr() + "<DT>");
621	* =>
622		closel(g);
623		g.list_type = Ldef;
624		g.print("<DL compact>\n" + g.softbr() + "<DT>");
625	}
626	g.hangingdt = level;
627	g.broken = true;
628}
629
630g_IP(g: ref Global, argl: list of string)
631{
632	case g.list_type {
633
634	Lordered or Lunordered or Lother =>
635		;	# continue with an existing list
636
637	* =>
638		# figure out the type of a new list and start it
639		closel(g);
640		arg := "";
641		if (argl != nil)
642			arg = hd argl;
643		case arg {
644			"1" or "i" or "I" or "a" or "A" =>
645				g.list_type = Lordered;
646				g.print(sprint("<OL type=%s>\n", arg));
647			"*" or "•" or "&#8226;" =>
648				g.list_type = Lunordered;
649				g.print("<UL type=disc>\n");
650			"○" or "&#9675;"=>
651				g.list_type = Lunordered;
652				g.print("<UL type=circle>\n");
653			"□" or "&#9633;" =>
654				g.list_type = Lunordered;
655				g.print("<UL type=square>\n");
656			* =>
657				g.list_type = Lother;
658				g.print("<DL compact>\n");
659			}
660	}
661
662	# actually do this list item
663	case g.list_type {
664	Lother =>
665		g.print(g.softp());	# make sure there's space before each list item
666		if (argl != nil) {
667			g.print("<DT>");
668			printargs(g, argl);
669		}
670		g.print("\n<DD>");
671
672	Lordered or Lunordered =>
673		g.print(g.softp() + "<LI>");
674	}
675	g.broken = true;
676}
677
678g_nf(g: ref Global)
679{
680	if (! g.fill)
681		return;
682	g.fill = false;
683	g.print("<PRE>\n");
684	g.broken = true;
685	g.sop = true;
686	g.pre = true;
687}
688
689g_PD(g: ref Global, argl: list of string)
690{
691	if (len argl == 1 && hd argl == "0")
692		g.ipd = false;
693	else
694		g.ipd = true;
695}
696
697g_PM(g: ref Global, argl: list of string)
698{
699	code := "P";
700	if (argl != nil)
701		code = hd argl;
702	case code {
703	* =>		# includes "1" and "P"
704		g.pm = "<B>Lucent Technologies - Proprietary</B>\n" +
705			"<BR>Use pursuant to Company Instructions.\n";
706	"2" or "RS" =>
707		g.pm = "<B>Lucent Technologies - Proprietary (Restricted)</B>\n" +
708			"<BR>Solely for authorized persons having a need to know\n" +
709			"<BR>pursuant to Company Instructions.\n";
710	"3" or "RG" =>
711		g.pm = "<B>Lucent Technologies - Proprietary (Registered)</B>\n" +
712			"<BR>Solely for authorized persons having a need to know\n" +
713			"<BR>and subject to cover sheet instructions.\n";
714	"4" or "CP" =>
715		g.pm = "SEE PROPRIETARY NOTICE ON COVER PAGE\n";
716	"5" or "CR" =>
717		g.pm = "Copyright xxxx Lucent Technologies\n" +	# should fill in the year from the date register
718			"<BR>All Rights Reserved.\n";
719	"6" or "UW" =>
720		g.pm = "THIS DOCUMENT CONTAINS PROPRIETARY INFORMATION OF\n" +
721			"<BR>LUCENT TECHNOLOGIES INC. AND IS NOT TO BE DISCLOSED OR USED EXCEPT IN\n" +
722			"<BR>ACCORDANCE WITH APPLICABLE AGREEMENTS.\n" +
723			"<BR>Unpublished & Not for Publication\n";
724	}
725}
726
727g_PP(g: ref Global)
728{
729	closel(g);
730	reset_font(g);
731	p := g.softp();
732	if (p != nil)
733		g.print(p);
734	g.sop = true;
735	g.broken = true;
736}
737
738g_RE(g: ref Global)
739{
740	g.print("</DL>\n");
741	g.indents--;
742	g.broken = true;
743}
744
745g_RS(g: ref Global)
746{
747	g.print("<DL>\n<DT><DD>");
748	g.indents++;
749	g.broken = true;
750}
751
752g_SH(g: ref Global, argl: list of string)
753{
754	closeall(g, 1);		# .SH is top-level list item
755	if (g.example)
756		g_EE(g);
757	g_fi(g);
758	if (g.fill && ! g.sop)
759		g.print("<P>");
760	g.print("<DT><H4>");
761	printargs(g, argl);
762	g.print("</H4>\n");
763	g.print("<DD>\n");
764	g.sop = true;
765	g.broken = true;
766}
767
768g_SM(g: ref Global, argl: list of string)
769{
770	g.def_sm++;		# can't use def_goobie, lest we collide with a deferred font macro
771	if (argl == nil)
772		return;
773	g.print(sprint("<FONT SIZE=-%d>", g.def_sm));
774	printargs(g, argl);
775	g.print("</FONT>\n");
776	g.def_sm = 0;
777}
778
779g_sp(g: ref Global, argl: list of string)
780{
781	if (g.sop && g.fill)
782		return;
783	count := 1;
784	if (argl != nil) {
785		rcount := real hd argl;
786		count = int rcount;	# may be 0 (e.g., ".sp .5")
787		if (count == 0 && rcount > 0.0)
788			count = 1;		# force whitespace for fractional lines
789	}
790	g.dobreak();
791	for (i := 0; i < count; i++)
792		g.print("&nbsp;<BR>\n");
793	g.broken = true;
794	g.sop = count > 0;
795}
796
797g_SS(g: ref Global, argl: list of string)
798{
799	closeall(g, 1);
800	g.indents++;
801	g.print(g.softp() + "<DL><DT><FONT SIZE=3><B>");
802	printargs(g, argl);
803	g.print("</B></FONT>\n");
804	g.print("<DD>\n");
805	g.sop = true;
806	g.broken = true;
807}
808
809g_TH(g: ref Global, argl: list of string)
810{
811	if (g.oldstyle.names && len argl > 2)
812		argl = hd argl :: hd tl argl :: nil;	# ignore extra .TH args on pages in oldstyle trees
813	case len argl {
814	0 =>
815		g.oldstyle.fmt = true;
816		title(g, sprint("%s", g.href.title), false);
817	1 =>
818		g.oldstyle.fmt = true;
819		title(g, sprint("%s", hd argl), false);	# any pages use this form?
820	2 =>
821		g.oldstyle.fmt = true;
822		g.thisone.page = hd argl;
823		g.thisone.mtype = hd tl argl;
824		g.mk_href_man(hd argl, true);
825		g.mk_href_mtype(nil, hd tl argl);
826		title(g, sprint("%s(%s)", g.href.man, g.href.mtype), false);
827	* =>
828		g.oldstyle.fmt = false;
829		chap := hd tl tl argl;
830		g.mk_href_chap(chap);
831		g.mk_href_man(hd argl, false);
832		g.mk_href_mtype(chap, hd tl argl);
833		title(g, sprint("%s/%s/%s(%s)", g.href.title, g.href.chap, g.href.man, g.href.mtype), false);
834	}
835	g.print("[<a href=\"../index.html\">manual index</a>]");
836	g.print("[<a href=\"INDEX.html\">section index</a>]<p>");
837	g.print("<DL>\n");	# whole man page is just one big list
838	g.indents = 1;
839	g.sop = true;
840	g.broken = true;
841}
842
843dohangingdt(g: ref Global)
844{
845	case g.hangingdt {
846	3 =>
847		g.hangingdt--;
848	2 =>
849		g.print("<DD>");
850		g.hangingdt = 0;
851		g.broken = true;
852	}
853}
854
855# close a list, if there's one active
856closel(g: ref Global)
857{
858	case g.list_type {
859	Lordered =>
860		g.print("</OL>\n");
861		g.broken = true;
862	Lunordered =>
863		g.print("</UL>\n");
864		g.broken = true;
865	Lother or Ldef =>
866		g.print("</DL>\n");
867		g.broken = true;
868	}
869	g.list_type = Lnone;
870}
871
872closeall(g: ref Global, level: int)
873{
874	closel(g);
875	reset_font(g);
876	while (g.indents > level) {
877		g.indents--;
878		g.print("</DL>\n");
879		g.broken = true;
880	}
881}
882
883#
884# Show last revision date for a file.
885#
886rev(g: ref Global, filebuf: ref Bufio->Iobuf)
887{
888	if (g.mtime == 0) {
889		(err, info) := sys->fstat(filebuf.fd);
890		if (! err)
891			g.mtime = info.mtime;
892	}
893	if (g.mtime != 0) {
894		g.print("<P><TABLE width=\"100%\" border=0 cellpadding=10 cellspacing=0 bgcolor=\"#E0E0E0\">\n");
895		g.print("<TR>");
896		g.print(sprint("<TD align=left><FONT SIZE=-1>"));
897		g.print(sprint("%s(%s)", g.thisone.page, g.thisone.mtype));
898		g.print("</FONT></TD>\n");
899		g.print(sprint("<TD align=right><FONT SIZE=-1><I>Rev:&nbsp;&nbsp;%s</I></FONT></TD></TR></TABLE>\n",
900			dt->text(dt->gmt(g.mtime))));
901	}
902}
903
904#
905# Some font alternation macros are references to other man pages;
906# detect them (second arg contains balanced parens) and make them into hot links.
907#
908anchor(g: ref Global, f1, f2: string, argl: list of string)
909{
910	final := "";
911	link := false;
912	if (len argl == 2) {
913		(s, e) := str->splitl(hd tl argl, ")");
914		if (str->prefix("(", s) && e != nil) {
915			# emit href containing search for target first
916			# if numeric, do old style
917			link = true;
918			file := hd argl;
919			(chap, man) := split(httpunesc(file), '/');
920			if (man == nil) {
921				# given no explicit chapter prefix, use current chapter
922				man = chap;
923				chap = g.thisone.chap;
924			}
925			mtype := s[1:];
926			if (mtype == nil)
927				mtype = "-";
928			(n, toks) := sys->tokenize(mtype, ".");	# Fix section 10
929			if (n > 1) mtype = hd toks;
930			g.print(sprint("<A href=\"../%s/%s.html\">", mtype, fixlink(man)));
931
932			#
933			# now generate the name the user sees, with terminal punctuation
934			# moved after the closing </A>.
935			#
936			if (len e > 1)
937				final = e[1:];
938			argl = hd argl :: s + ")" :: nil;
939		}
940	}
941	altfont(g, f1, f2, argl, false);
942	if (link) {
943		g.print("</A>");
944		font(g, f2, final :: nil);
945	} else
946		g.print("\n");
947}
948
949
950#
951# Fix up a link
952#
953
954fixlink(l: string): string
955{
956	ll := str->tolower(l);
957	if (ll == "copyright") ll = "1" + ll;
958	(a, b) := str->splitstrl(ll, "intro");
959	if (len b == 5) ll = a + "0" + b;
960	return ll;
961}
962
963
964#
965# output argl in font f
966#
967font(g: ref Global, f: string, argl: list of string)
968{
969	if (argl == nil) {
970		g.def_goobie = f;
971		return;
972	}
973	case f {
974	"L" => 	f = "TT";
975	"R" =>	f = nil;
976	}
977	if (f != nil) 			# nil == default (typically Roman)
978		g.print(sprint("<%s>", f));
979	printargs(g, argl);
980	if (f != nil)
981		g.print(sprint("</%s>", f));
982	g.print("\n");
983	g.prevfont = f;
984}
985
986#
987# output concatenated elements of argl, alternating between fonts f1 and f2
988#
989altfont(g: ref Global, f1, f2: string, argl: list of string, newline: int)
990{
991	reset_font(g);
992	if (argl == nil) {
993		g.def_goobie = f1;
994		return;
995	}
996	case f1 {
997	"L" =>	f1 = "TT";
998	"R" =>	f1 = nil;
999	}
1000	case f2 {
1001	"L" =>	f2 = "TT";
1002	"R" =>	f2 = nil;
1003	}
1004	f := f1;
1005	for (; argl != nil; argl = tl argl) {
1006		if (f != nil)
1007			g.print(sprint("<%s>%s</%s>", f, hd argl, f));
1008		else
1009			g.print(hd argl);
1010		if (f == f1)
1011			f = f2;
1012		else
1013			f = f1;
1014	}
1015	if (newline)
1016		g.print("\n");
1017	g.prevfont = f;
1018}
1019
1020# not yet implemented
1021map_font(nil: ref Global, nil: string)
1022{
1023}
1024
1025reset_font(g: ref Global)
1026{
1027	if (g.curfont != nil) {
1028		g.print(sprint("</%s>", g.curfont));
1029		g.prevfont = g.curfont;
1030		g.curfont = nil;
1031	}
1032}
1033
1034printargs(g: ref Global, argl: list of string)
1035{
1036	for (; argl != nil; argl = tl argl)
1037		if (tl argl != nil)
1038			g.print(hd argl + " ");
1039		else
1040			g.print(hd argl);
1041}
1042
1043# any parameter can be nil
1044addhit(g: ref Global, chap, mtype, page: string)
1045{
1046	# g.print(sprint("Adding %s / %s (%s) . . .", chap, page, mtype));		# debug
1047	# always keep a spare slot at the end
1048	if (g.nhits >= len g.hits - 1)
1049		g.hits = (array[len g.hits + 32] of Hit)[0:] = g.hits;
1050	g.hits[g.nhits].glob = chap + " " + mtype + " " + page;
1051	g.hits[g.nhits].chap = chap;
1052	g.hits[g.nhits].mtype = mtype;
1053	g.hits[g.nhits++].page = page;
1054}
1055
1056Global.dobreak(g: self ref Global)
1057{
1058	if (! g.broken) {
1059		g.broken = true;
1060		g.print("<BR>\n");
1061	}
1062}
1063
1064Global.print(g: self ref Global, s: string)
1065{
1066	g.bufio->g.bout.puts(s);
1067	if (g.sop || g.broken) {
1068		# first non-white space, non-HTML we print takes us past the start of the paragraph & line
1069		# (or even white space, if we're in no-fill mode)
1070		for (i := 0; i < len s; i++) {
1071			case s[i] {
1072			'<' =>
1073				while (++i < len s && s[i] != '>')
1074					;
1075				continue;
1076			' ' or '\t' or '\n' =>
1077				if (g.fill)
1078					continue;
1079			}
1080			g.sop = false;
1081			g.broken = false;
1082			break;
1083		}
1084	}
1085}
1086
1087Global.softbr(g: self ref Global): string
1088{
1089	if (g.broken)
1090		return nil;
1091	g.broken = true;
1092	return "<BR>";
1093}
1094
1095# provide a paragraph marker, unless we're already at the start of a section
1096Global.softp(g: self ref Global): string
1097{
1098	if (g.sop)
1099		return nil;
1100	else if (! g.ipd)
1101		return "<BR>";
1102	if (g.fill)
1103		return "<P>";
1104	else
1105		return "<P style=\"white-space: pre\">";
1106}
1107
1108#
1109# get (remainder of) a line
1110#
1111getline(g: ref Global): string
1112{
1113	line := "";
1114	while ((token := getnext(g)) != "\n") {
1115		if (token == nil)
1116			return line;
1117		line += token;
1118	}
1119	return line+"\n";
1120}
1121
1122#
1123# Get next logical character.  Expand it with escapes.
1124#
1125getnext(g: ref Global): string
1126{
1127	iob := g.bufio;
1128	Iobuf: import iob;
1129
1130	font: string;
1131	token: string;
1132	bin := g.bin;
1133
1134	g.sol = (g.lastc == '\n');
1135
1136	c := bin.getc();
1137	if (c < 0)
1138		return nil;
1139	g.lastc = c;
1140	if (c >= Runeself) {
1141		for (i := 0;  i < len Entities; i++)
1142			if (Entities[i].value == c)
1143				return Entities[i].name;
1144		return sprint("&#%d;", c);
1145	}
1146	case c {
1147	'<' =>
1148		return "&lt;";
1149	'>' =>
1150		return "&gt;";
1151	'\\' =>
1152		c = bin.getc();
1153		if (c < 0)
1154			return nil;
1155		g.lastc = c;
1156		case c {
1157
1158		' ' =>
1159			return "&nbsp;";
1160
1161		# chars to ignore
1162		'|' or '&' or '^' =>
1163			return getnext(g);
1164
1165		# ignore arg
1166		'k' =>
1167			nil = bin.getc();
1168			return getnext(g);
1169
1170		# defined strings
1171		'*' =>
1172			case bin.getc() {
1173			'R' =>
1174				return "&#174;";
1175			}
1176			return getnext(g);
1177
1178		# special chars
1179		'(' =>
1180			token[0] = bin.getc();
1181			token[1] = bin.getc();
1182			for (i := 0; i < len tspec; i++)
1183				if (token == tspec[i].name)
1184					return tspec[i].value;
1185			return "&#191;";
1186		'c' =>
1187			c = bin.getc();
1188			if (c < 0)
1189				return nil;
1190			else if (c == '\n') {
1191				g.lastc = c;
1192				g.sol = true;
1193				token[0] = bin.getc();
1194				return token;
1195			}
1196			# DEBUG: should there be a "return xxx" here?
1197		'e' =>
1198			return "\\";
1199		'f' =>
1200			g.lastc = c = bin.getc();
1201			if (c < 0)
1202				return nil;
1203			case c {
1204			'2' or 	'I' =>
1205				font = "I";
1206			'3' or 	'B' =>
1207				font = "B";
1208			'5' or 	'L' =>
1209				font = "TT";
1210			'P' =>
1211				font = g.prevfont;
1212			* =>					# includes '1' and 'R'
1213				font = nil;
1214			}
1215# There are serious problems with this. We don't know the fonts properly at this stage.
1216#			g.prevfont = g.curfont;
1217#			g.curfont = font;
1218#			if (g.prevfont != nil)
1219#				token = sprint("</%s>", g.prevfont);
1220#			if (g.curfont != nil)
1221#				token += sprint("<%s>", g.curfont);
1222			if (token == nil)
1223				return "<i></i>";	# looks odd but it avoids inserting a space in <pre> text
1224			return token;
1225		's' =>
1226			sign := '+';
1227			size := 0;
1228			relative := false;
1229		getsize:
1230			for (;;) {
1231				c = bin.getc();
1232				if (c < 0)
1233					return nil;
1234				case c {
1235				'+' =>
1236					relative = true;
1237				'-' =>
1238					sign = '-';
1239					relative = true;
1240				'0' to '9' =>
1241					size = size * 10 + (c - '0');
1242				* =>
1243					bin.ungetc();
1244					break getsize;
1245				}
1246				g.lastc = c;
1247			}
1248			if (size == 0)
1249				token = "</FONT>";
1250			else if (relative)
1251				token = sprint("<FONT SIZE=%c%d>", sign, size);
1252			else
1253				token = sprint("<FONT SIZE=%d>", size);
1254			return token;
1255		}
1256	}
1257	token[0] = c;
1258	return token;
1259}
1260
1261#
1262# Return strings before and after the left-most instance of separator;
1263# (s, nil) if no match or separator is last char in s.
1264#
1265split(s: string, sep: int): (string, string)
1266{
1267	for (i := 0; i < len s; i++)
1268		if (s[i] == sep)
1269			return (s[:i], s[i+1:]);	# s[len s:] is a valid slice, with value == nil
1270 	return (s, nil);
1271}
1272
1273Global_init(): ref Global
1274{
1275	g := ref Global;
1276	g.bufio = load Bufio Bufio->PATH;
1277	g.chaps = array[20] of Chaps;
1278	g.types = array[20] of Types;
1279	g.mantitle = "";
1280	g.href.title = g.mantitle;		# ??
1281	g.mtime = 0;
1282	g.nhits = 0;
1283	g.oldstyle.names = false;
1284	g.oldstyle.fmt = false;
1285	g.topname = "System";
1286	g.list_type = Lnone;
1287	g.def_sm = 0;
1288	g.hangingdt = 0;
1289	g.indents = 0;
1290	g.sop = true;
1291	g.broken = true;
1292	g.ipd = true;
1293	g.fill = true;
1294	g.example = false;
1295	g.pre = false;
1296	g.lastc = '\n';
1297	return g;
1298}
1299
1300Global.mk_href_chap(g: self ref Global, chap: string)
1301{
1302	if (chap != nil)
1303		g.href.chap = sprint("<A href=\"%s/%s?man=*\"><B>%s</B></A>", g.mandir, chap, chap);
1304}
1305
1306Global.mk_href_man(g: self ref Global, man: string, oldstyle: int)
1307{
1308	rman := man;
1309	if (oldstyle)
1310		rman = str->tolower(man);	# compensate for tradition of putting titles in all CAPS
1311	g.href.man = sprint("<A href=\"%s?man=%s\"><B>%s</B></A>", g.mandir, rman, man);
1312}
1313
1314Global.mk_href_mtype(g: self ref Global, chap, mtype: string)
1315{
1316	g.href.mtype = sprint("<A href=\"%s/%s/%s\"><B>%s</B></A>", g.mandir, chap, mtype, mtype);
1317}
1318
1319# We assume that anything >= Runeself is already in UTF.
1320#
1321httpunesc(s: string): string
1322{
1323	t := "";
1324	for (i := 0; i < len s; i++) {
1325		c := s[i];
1326		if (c == '&' && i + 1 < len s) {
1327			(char, rem) := str->splitl(s[i+1:], ";");
1328			if (rem == nil)
1329				break;	# require the terminating ';'
1330			if (char == nil)
1331				continue;
1332			if (char[0] == '#' && len char > 1) {
1333				c = int char[1:];
1334				i += len char;
1335				if (c < 256 && c >= 161) {
1336					t[len t] = Entities[c-161].value;
1337					continue;
1338				}
1339			} else {
1340				for (j := 0; j < len Entities; j++)
1341					if (Entities[j].name == char)
1342						break;
1343				if (j < len Entities) {
1344					i += len char;
1345					t[len t] = Entities[j].value;
1346					continue;
1347				}
1348			}
1349		}
1350		t[len t] = c;
1351	}
1352	return t;
1353}
1354
1355
1356
1357title(g: ref Global, t: string, search: int)
1358{
1359	if(search)
1360		;	# not yet used
1361	g.print(header+"\n");
1362	g.print(sprint("<TITLE>Inferno's %s</TITLE>\n", demark(t)));
1363	g.print("</HEAD>\n");
1364	g.print("<BODY>"+initial+"\n");
1365
1366}
1367