texi2html.py revision 26a9d37e5c473f1489553ec9e0e8c0444a88ae9f
1#! /usr/local/bin/python
2
3# Convert GNU texinfo files into HTML, one file per node.
4# Based on Texinfo 2.14.
5# Usage: texi2html [-d] [-d] inputfile outputdirectory
6# The input file must be a complete texinfo file, e.g. emacs.texi.
7# This creates many files (one per info node) in the output directory,
8# overwriting existing files of the same name.  All files created have
9# ".html" as their extension.
10
11
12# XXX To do:
13# - handle @comment*** correctly
14# - handle @xref {some words} correctly
15# - handle @ftable correctly (items aren't indexed?)
16# - handle @itemx properly
17# - handle @exdent properly
18# - add links directly to the proper line from indices
19# - check against the definitive list of @-cmds; we still miss (among others):
20# - @set, @clear, @ifset, @ifclear
21# - @defindex (hard)
22# - @c(omment) in the middle of a line (rarely used)
23# - @this* (not really needed, only used in headers anyway)
24# - @today{} (ever used outside title page?)
25
26
27import os
28import regex
29import regsub
30import string
31
32MAGIC = '\\input texinfo'
33
34cmprog = regex.compile('^@\([a-z]+\)\([ \t]\|$\)') # Command (line-oriented)
35blprog = regex.compile('^[ \t]*$') # Blank line
36kwprog = regex.compile('@[a-z]+') # Keyword (embedded, usually with {} args)
37spprog = regex.compile('[\n@{}&<>]') # Special characters in running text
38miprog = regex.compile( \
39	'^\* \([^:]*\):\(:\|[ \t]*\([^\t,\n.]+\)\([^ \t\n]*\)\)[ \t\n]*')
40					# menu item (Yuck!)
41
42class TexinfoParser:
43
44	# Initialize an instance
45	def __init__(self):
46		self.unknown = {}	# statistics about unknown @-commands
47		self.debugging = 0	# larger values produce more output
48		self.nodefp = None	# open file we're writing to
49		self.savetext = None	# If not None, save text head instead
50		self.dirname = 'tmp'	# directory where files are created
51		self.includedir = '.'	# directory to search @include files
52		self.nodename = ''	# name of current node
53		self.topname = ''	# name of top node (first node seen)
54		self.title = ''		# title of this whole Texinfo tree
55		self.resetindex()	# Reset all indices
56		self.contents = []	# Reset table of contents
57		self.numbering = []	# Reset section numbering counters
58		self.nofill = 0		# Normal operation: fill paragraphs
59		# XXX The following should be reset per node?!
60		self.footnotes = []	# Reset list of footnotes
61		self.itemarg = None	# Reset command used by @item
62		self.itemnumber = None	# Reset number for @item in @enumerate
63		self.itemindex = None	# Reset item index name
64
65	# Set (output) directory name
66	def setdirname(self, dirname):
67		self.dirname = dirname
68
69	# Set include directory name
70	def setincludedir(self, includedir):
71		self.includedir = includedir
72
73	# Parse the contents of an entire file
74	def parse(self, fp):
75		line = fp.readline()
76		lineno = 1
77		while line and (line[0] == '%' or blprog.match(line) >= 0):
78			line = fp.readline()
79			lineno = lineno + 1
80		if line[:len(MAGIC)] <> MAGIC:
81			raise SyntaxError, 'file does not begin with '+`MAGIC`
82		self.parserest(fp, lineno)
83
84	# Parse the contents of a file, not expecting a MAGIC header
85	def parserest(self, fp, initial_lineno):
86		lineno = initial_lineno
87		self.done = 0
88		self.skip = 0
89		self.stack = []
90		accu = []
91		while not self.done:
92			line = fp.readline()
93			if not line:
94				if accu:
95					if not self.skip: self.process(accu)
96					accu = []
97				if initial_lineno > 0:
98					print '*** EOF before @bye'
99				break
100			lineno = lineno + 1
101			if cmprog.match(line) >= 0:
102				a, b = cmprog.regs[1]
103				cmd = line[a:b]
104				if cmd in ('noindent', 'refill'):
105					accu.append(line)
106				else:
107					if accu:
108						if not self.skip:
109							self.process(accu)
110						accu = []
111					self.command(line)
112			elif blprog.match(line) >= 0:
113				if accu:
114					if not self.skip:
115						self.process(accu)
116						self.write('<P>\n')
117					accu = []
118			else:
119				# Append the line including trailing \n!
120				accu.append(line)
121		#
122		if self.skip:
123			print '*** Still skipping at the end'
124		if self.stack:
125			print '*** Stack not empty at the end'
126			print '***', self.stack
127
128	# Start saving text in a buffer instead of writing it to a file
129	def startsaving(self):
130		if self.savetext <> None:
131			print '*** Recursively saving text, expect trouble'
132		self.savetext = ''
133
134	# Return the text saved so far and start writing to file again
135	def collectsavings(self):
136		savetext = self.savetext
137		self.savetext = None
138		return savetext or ''
139
140	# Write text to file, or save it in a buffer, or ignore it
141	def write(self, *args):
142		text = string.joinfields(args, '')
143		if self.savetext <> None:
144			self.savetext = self.savetext + text
145		elif self.nodefp:
146			self.nodefp.write(text)
147
148	# Complete the current node -- write footnotes and close file
149	def endnode(self):
150		if self.savetext <> None:
151			print '*** Still saving text at end of node'
152			dummy = self.collectsavings()
153		if self.footnotes:
154			self.writefootnotes()
155		if self.nodefp:
156			self.nodefp.close()
157		self.nodefp = None
158		self.nodename = ''
159
160	# Process a list of lines, expanding embedded @-commands
161	# This mostly distinguishes between menus and normal text
162	def process(self, accu):
163		if self.debugging > 1:
164			print self.skip, self.stack,
165			if accu: print accu[0][:30],
166			if accu[0][30:] or accu[1:]: print '...',
167			print
168		if self.stack and self.stack[-1] == 'menu':
169			# XXX should be done differently
170			for line in accu:
171				if miprog.match(line) < 0:
172					line = string.strip(line) + '\n'
173					self.expand(line)
174					continue
175				(bgn, end), (a, b), (c, d), (e, f), (g, h) = \
176					miprog.regs[:5]
177				label = line[a:b]
178				nodename = line[c:d]
179				if nodename[0] == ':': nodename = label
180				else: nodename = line[e:f]
181				punct = line[g:h]
182				self.write('<DT><A HREF="', \
183					makefile(nodename), \
184					'" TYPE=Menu>', nodename, \
185					'</A>', punct, '\n<DD>')
186				self.expand(line[end:])
187		else:
188			text = string.joinfields(accu, '')
189			self.expand(text)
190
191	# Write a string, expanding embedded @-commands
192	def expand(self, text):
193		stack = []
194		i = 0
195		n = len(text)
196		while i < n:
197			start = i
198			i = spprog.search(text, i)
199			if i < 0:
200				self.write(text[start:])
201				break
202			self.write(text[start:i])
203			c = text[i]
204			i = i+1
205			if c == '\n':
206				if self.nofill > 0:
207					self.write('<P>\n')
208				else:
209					self.write('\n')
210				continue
211			if c == '<':
212				self.write('&lt;')
213				continue
214			if c == '>':
215				self.write('&gt;')
216				continue
217			if c == '&':
218				self.write('&amp;')
219				continue
220			if c == '{':
221				stack.append('')
222				continue
223			if c == '}':
224				if not stack:
225					print '*** Unmatched }'
226					self.write('}')
227					continue
228				cmd = stack[-1]
229				del stack[-1]
230				try:
231					method = getattr(self, 'close_' + cmd)
232				except AttributeError:
233					self.unknown_close(cmd)
234					continue
235				method()
236				continue
237			if c <> '@':
238				# Cannot happen unless spprog is changed
239				raise RuntimeError, 'unexpected funny '+`c`
240			start = i
241			while i < n and text[i] in string.letters: i = i+1
242			if i == start:
243				# @ plus non-letter: literal next character
244				i = i+1
245				c = text[start:i]
246				if c == ':':
247					# `@:' means no extra space after
248					# preceding `.', `?', `!' or `:'
249					pass
250				else:
251					# `@.' means a sentence-ending period;
252					# `@@', `@{', `@}' quote `@', `{', `}'
253					self.write(c)
254				continue
255			cmd = text[start:i]
256			if i < n and text[i] == '{':
257				i = i+1
258				stack.append(cmd)
259				try:
260					method = getattr(self, 'open_' + cmd)
261				except AttributeError:
262					self.unknown_open(cmd)
263					continue
264				method()
265				continue
266			try:
267				method = getattr(self, 'handle_' + cmd)
268			except AttributeError:
269				self.unknown_handle(cmd)
270				continue
271			method()
272		if stack:
273			print '*** Stack not empty at para:', stack
274
275	# --- Handle unknown embedded @-commands ---
276
277	def unknown_open(self, cmd):
278		print '*** No open func for @' + cmd + '{...}'
279		cmd = cmd + '{'
280		self.write('@', cmd)
281		if not self.unknown.has_key(cmd):
282			self.unknown[cmd] = 1
283		else:
284			self.unknown[cmd] = self.unknown[cmd] + 1
285
286	def unknown_close(self, cmd):
287		print '*** No close func for @' + cmd + '{...}'
288		cmd = '}' + cmd
289		self.write('}')
290		if not self.unknown.has_key(cmd):
291			self.unknown[cmd] = 1
292		else:
293			self.unknown[cmd] = self.unknown[cmd] + 1
294
295	def unknown_handle(self, cmd):
296		print '*** No handler for @' + cmd
297		self.write('@', cmd)
298		if not self.unknown.has_key(cmd):
299			self.unknown[cmd] = 1
300		else:
301			self.unknown[cmd] = self.unknown[cmd] + 1
302
303	# XXX The following sections should be ordered as the texinfo docs
304
305	# --- Embedded @-commands without {} argument list --
306
307	def handle_noindent(self): pass
308
309	def handle_refill(self): pass
310
311	# --- Include file handling ---
312
313	def do_include(self, args):
314		file = args
315		file = os.path.join(self.includedir, file)
316		try:
317			fp = open(file, 'r')
318		except IOError, msg:
319			print '*** Can\'t open include file', `file`
320			return
321		if self.debugging:
322			print '--> file', `file`
323		save_done = self.done
324		save_skip = self.skip
325		save_stack = self.stack
326		self.parserest(fp, 0)
327		fp.close()
328		self.done = save_done
329		self.skip = save_skip
330		self.stack = save_stack
331		if self.debugging:
332			print '<-- file', `file`
333
334	# --- Special Insertions ---
335
336	def open_dmn(self): pass
337	def close_dmn(self): pass
338
339	def open_dots(self): self.write('...')
340	def close_dots(self): pass
341
342	def open_bullet(self): self.write('&bullet;')
343	def close_bullet(self): pass
344
345	def open_TeX(self): self.write('TeX')
346	def close_TeX(self): pass
347
348	def open_copyright(self): self.write('(C)')
349	def close_copyright(self): pass
350
351	def open_minus(self): self.write('-')
352	def close_minus(self): pass
353
354	# --- Special Glyphs for Examples ---
355
356	def open_result(self): self.write('=&gt;')
357	def close_result(self): pass
358
359	def open_expansion(self): self.write('==&gt;')
360	def close_expansion(self): pass
361
362	def open_print(self): self.write('-|')
363	def close_print(self): pass
364
365	def open_error(self): self.write('error--&gt;')
366	def close_error(self): pass
367
368	def open_equiv(self): self.write('==')
369	def close_equiv(self): pass
370
371	def open_point(self): self.write('-!-')
372	def close_point(self): pass
373
374	# --- Cross References ---
375
376	def open_pxref(self):
377		self.write('see ')
378		self.startsaving()
379	def close_pxref(self):
380		self.makeref()
381
382	def open_xref(self):
383		self.write('See ')
384		self.startsaving()
385	def close_xref(self):
386		self.makeref()
387
388	def open_ref(self):
389		self.startsaving()
390	def close_ref(self):
391		self.makeref()
392
393	def open_inforef(self):
394		self.write('See info file ')
395		self.startsaving()
396	def close_inforef(self):
397		text = self.collectsavings()
398		args = string.splitfields(text, ',')
399		n = len(args)
400		for i in range(n):
401			args[i] = string.strip(args[i])
402		while len(args) < 3: args.append('')
403		node = args[0]
404		file = args[2]
405		self.write('`', file, '\', node `', node, '\'')
406
407	def makeref(self):
408		text = self.collectsavings()
409		args = string.splitfields(text, ',')
410		n = len(args)
411		for i in range(n):
412			args[i] = string.strip(args[i])
413		while len(args) < 5: args.append('')
414		nodename = label = args[0]
415		if args[2]: label = args[2]
416		file = args[3]
417		title = args[4]
418		href = makefile(nodename)
419		if file:
420			href = '../' + file + '/' + href
421		self.write('<A HREF="', href, '">', label, '</A>')
422
423	# --- Marking Words and Phrases ---
424
425	# --- Other @xxx{...} commands ---
426
427	def open_(self): pass # Used by {text enclosed in braces}
428	def close_(self): pass
429
430	open_asis = open_
431	close_asis = close_
432
433	def open_cite(self): self.write('<CITE>')
434	def close_cite(self): self.write('</CITE>')
435
436	def open_code(self): self.write('<CODE>')
437	def close_code(self): self.write('</CODE>')
438
439	open_t = open_code
440	close_t = close_code
441
442	def open_dfn(self): self.write('<DFN>')
443	def close_dfn(self): self.write('</DFN>')
444
445	def open_emph(self): self.write('<I>')
446	def close_emph(self): self.write('</I>')
447
448	open_i = open_emph
449	close_i = close_emph
450
451	def open_footnote(self):
452		if self.savetext <> None:
453			print '*** Recursive footnote -- expect weirdness'
454		id = len(self.footnotes) + 1
455		self.write('<A NAME="footnoteref', `id`, \
456			'" HREF="#footnotetext', `id`, '">(', `id`, ')</A>')
457		self.savetext = ''
458
459	def close_footnote(self):
460		id = len(self.footnotes) + 1
461		self.footnotes.append(`id`, self.savetext)
462		self.savetext = None
463
464	def writefootnotes(self):
465		self.write('<H2>---------- Footnotes ----------</H2>\n')
466		for id, text in self.footnotes:
467			self.write('<A NAME="footnotetext', id, \
468				'" HREF="#footnoteref', id, '">(', \
469				id, ')</A>\n', text, '<P>\n')
470		self.footnotes = []
471
472	def open_file(self): self.write('<FILE>')
473	def close_file(self): self.write('</FILE>')
474
475	def open_kbd(self): self.write('<KBD>')
476	def close_kbd(self): self.write('</KBD>')
477
478	def open_key(self): self.write('<KEY>')
479	def close_key(self): self.write('</KEY>')
480
481	def open_r(self): self.write('<R>')
482	def close_r(self): self.write('</R>')
483
484	def open_samp(self): self.write('`<SAMP>')
485	def close_samp(self): self.write('</SAMP>\'')
486
487	def open_sc(self): self.write('<SMALLCAPS>')
488	def close_sc(self): self.write('</SMALLCAPS>')
489
490	def open_strong(self): self.write('<B>')
491	def close_strong(self): self.write('</B>')
492
493	open_b = open_strong
494	close_b = close_strong
495
496	def open_var(self): self.write('<VAR>')
497	def close_var(self): self.write('</VAR>')
498
499	def open_w(self): self.write('<NOBREAK>')
500	def close_w(self): self.write('</NOBREAK>')
501
502	open_titlefont = open_
503	close_titlefont = close_
504
505	def command(self, line):
506		a, b = cmprog.regs[1]
507		cmd = line[a:b]
508		args = string.strip(line[b:])
509		if self.debugging > 1:
510			print self.skip, self.stack, '@' + cmd, args
511		try:
512			func = getattr(self, 'do_' + cmd)
513		except AttributeError:
514			try:
515				func = getattr(self, 'bgn_' + cmd)
516			except AttributeError:
517				self.unknown_cmd(cmd, args)
518				return
519			self.stack.append(cmd)
520			func(args)
521			return
522		if not self.skip or cmd == 'end':
523			func(args)
524
525	def unknown_cmd(self, cmd, args):
526		print '*** unknown', '@' + cmd, args
527		if not self.unknown.has_key(cmd):
528			self.unknown[cmd] = 1
529		else:
530			self.unknown[cmd] = self.unknown[cmd] + 1
531
532	def do_end(self, args):
533		words = string.split(args)
534		if not words:
535			print '*** @end w/o args'
536		else:
537			cmd = words[0]
538			if not self.stack or self.stack[-1] <> cmd:
539				print '*** @end', cmd, 'unexpected'
540			else:
541				del self.stack[-1]
542			try:
543				func = getattr(self, 'end_' + cmd)
544			except AttributeError:
545				self.unknown_end(cmd)
546				return
547			func()
548
549	def unknown_end(self, cmd):
550		cmd = 'end ' + cmd
551		print '*** unknown', '@' + cmd
552		if not self.unknown.has_key(cmd):
553			self.unknown[cmd] = 1
554		else:
555			self.unknown[cmd] = self.unknown[cmd] + 1
556
557	# --- Comments ---
558
559	def do_comment(self, args): pass
560	do_c = do_comment
561
562	# --- Conditional processing ---
563
564	def bgn_ifinfo(self, args): pass
565	def end_ifinfo(self): pass
566
567	def bgn_iftex(self, args): self.skip = self.skip + 1
568	def end_iftex(self): self.skip = self.skip - 1
569
570	def bgn_ignore(self, args): self.skip = self.skip + 1
571	def end_ignore(self): self.skip = self.skip - 1
572
573	def bgn_tex(self, args): self.skip = self.skip + 1
574	def end_tex(self): self.skip = self.skip - 1
575
576	# --- Beginning a file ---
577
578	do_finalout = do_comment
579	do_setchapternewpage = do_comment
580	do_setfilename = do_comment
581
582	def do_settitle(self, args):
583		self.title = args
584
585	# --- Ending a file ---
586
587	def do_bye(self, args):
588		self.done = 1
589
590	# --- Title page ---
591
592	def bgn_titlepage(self, args): self.skip = self.skip + 1
593	def end_titlepage(self): self.skip = self.skip - 1
594
595	def do_center(self, args):
596		# Actually not used outside title page...
597		self.write('<H1>', args, '</H1>\n')
598	do_title = do_center
599	do_subtitle = do_center
600	do_author = do_center
601
602	do_vskip = do_comment
603	do_vfill = do_comment
604	do_smallbook = do_comment
605
606	do_paragraphindent = do_comment
607	do_setchapternewpage = do_comment
608	do_headings = do_comment
609	do_footnotestyle = do_comment
610
611	do_evenheading = do_comment
612	do_evenfooting = do_comment
613	do_oddheading = do_comment
614	do_oddfooting = do_comment
615	do_everyheading = do_comment
616	do_everyfooting = do_comment
617
618	# --- Nodes ---
619
620	def do_node(self, args):
621		parts = string.splitfields(args, ',')
622		while len(parts) < 4: parts.append('')
623		for i in range(4): parts[i] = string.strip(parts[i])
624		[name, next, prev, up] = parts[:4]
625		self.endnode()
626		file = self.dirname + '/' + makefile(name)
627		if self.debugging: print '--- writing', file
628		self.nodefp = open(file, 'w')
629		self.nodename = name
630		if not self.topname: self.topname = name
631		title = name
632		if self.title: title = title + ' -- ' + self.title
633		self.write('<TITLE>', title, '</TITLE>\n')
634		self.link('Next', next)
635		self.link('Prev', prev)
636		self.link('Up', up)
637		if self.nodename <> self.topname:
638			self.link('Top', self.topname)
639
640	def link(self, label, nodename):
641		if nodename:
642			if string.lower(nodename) == '(dir)':
643				addr = '../dir.html'
644			else:
645				addr = makefile(nodename)
646			self.write(label, ': <A HREF="', addr, '" TYPE="', \
647				label, '">', nodename, '</A>  \n')
648
649	# --- Sectioning commands ---
650
651	def do_chapter(self, args):
652		self.heading('H1', args, 0)
653	def do_unnumbered(self, args):
654		self.heading('H1', args, -1)
655	def do_appendix(self, args):
656		self.heading('H1', args, -1)
657	def do_top(self, args):
658		self.heading('H1', args, -1)
659	def do_chapheading(self, args):
660		self.heading('H1', args, -1)
661	def do_majorheading(self, args):
662		self.heading('H1', args, -1)
663
664	def do_section(self, args):
665		self.heading('H1', args, 1)
666	def do_unnumberedsec(self, args):
667		self.heading('H1', args, -1)
668	def do_appendixsec(self, args):
669		self.heading('H1', args, -1)
670	do_appendixsection = do_appendixsec
671	def do_heading(self, args):
672		self.heading('H1', args, -1)
673
674	def do_subsection(self, args):
675		self.heading('H2', args, 2)
676	def do_unnumberedsubsec(self, args):
677		self.heading('H2', args, -1)
678	def do_appendixsubsec(self, args):
679		self.heading('H2', args, -1)
680	def do_subheading(self, args):
681		self.heading('H2', args, -1)
682
683	def do_subsubsection(self, args):
684		self.heading('H3', args, 3)
685	def do_unnumberedsubsubsec(self, args):
686		self.heading('H3', args, -1)
687	def do_appendixsubsubsec(self, args):
688		self.heading('H3', args, -1)
689	def do_subsubheading(self, args):
690		self.heading('H3', args, -1)
691
692	def heading(self, type, args, level):
693		if level >= 0:
694			while len(self.numbering) <= level:
695				self.numbering.append(0)
696			del self.numbering[level+1:]
697			self.numbering[level] = self.numbering[level] + 1
698			x = ''
699			for i in self.numbering:
700				x = x + `i` + '.'
701			args = x + ' ' + args
702			self.contents.append(level, args, self.nodename)
703		self.write('<', type, '>')
704		self.expand(args)
705		self.write('</', type, '>\n')
706		if self.debugging:
707			print '---', args
708
709	def do_contents(self, args):
710		pass
711		# self.listcontents('Table of Contents', 999)
712
713	def do_shortcontents(self, args):
714		pass
715		# self.listcontents('Short Contents', 0)
716	do_summarycontents = do_shortcontents
717
718	def listcontents(self, title, maxlevel):
719		self.write('<H1>', title, '</H1>\n<UL COMPACT>\n')
720		for level, title, node in self.contents:
721			if level <= maxlevel:
722				self.write('<LI>', '.   '*level, '<A HREF="', \
723					makefile(node), '">')
724				self.expand(title)
725				self.write('</A> ', node, '\n')
726		self.write('</UL>\n')
727
728	# --- Page lay-out ---
729
730	# These commands are only meaningful in printed text
731
732	def do_page(self, args): pass
733
734	def do_need(self, args): pass
735
736	def bgn_group(self, args): pass
737	def end_group(self): pass
738
739	# --- Line lay-out ---
740
741	def do_sp(self, args):
742		# Insert <args> blank lines
743		if args:
744			try:
745				n = string.atoi(args)
746			except string.atoi_error:
747				n = 1
748		else:
749			n = 1
750		self.write('<P>\n'*max(n, 0))
751
752	# --- Function and variable definitions ---
753
754	def bgn_deffn(self, args):
755		self.write('<DL><DT>')
756		words = splitwords(args, 2)
757		[category, name], rest = words[:2], words[2:]
758		self.expand('@b{' + name + '}')
759		for word in rest: self.expand(' ' + makevar(word))
760		self.expand(' -- ' + category)
761		self.write('<DD>\n')
762		self.index('fn', name)
763
764	def end_deffn(self):
765		self.write('</DL>\n')
766
767	def bgn_defun(self, args): self.bgn_deffn('Function ' + args)
768	end_defun = end_deffn
769
770	def bgn_defmac(self, args): self.bgn_deffn('Macro ' + args)
771	end_defmac = end_deffn
772
773	def bgn_defspec(self, args): self.bgn_deffn('{Special Form} ' + args)
774	end_defspec = end_deffn
775
776	def bgn_defvr(self, args):
777		self.write('<DL><DT>')
778		words = splitwords(args, 2)
779		[category, name], rest = words[:2], words[2:]
780		self.expand('@code{' + name + '}')
781		# If there are too many arguments, show them
782		for word in rest: self.expand(' ' + word)
783		self.expand(' -- ' + category)
784		self.write('<DD>\n')
785		self.index('vr', name)
786
787	end_defvr = end_deffn
788
789	def bgn_defvar(self, args): self.bgn_defvr('Variable ' + args)
790	end_defvar = end_defvr
791
792	def bgn_defopt(self, args): self.bgn_defvr('{User Option} ' + args)
793	end_defopt = end_defvr
794
795	# --- Ditto for typed languages ---
796
797	def bgn_deftypefn(self, args):
798		self.write('<DL><DT>')
799		words = splitwords(args, 3)
800		[category, datatype, name], rest = words[:3], words[3:]
801		self.expand('@code{' + datatype + '} @b{' + name + '}')
802		for word in rest: self.expand(' ' + makevar(word))
803		self.expand(' -- ' + category)
804		self.write('<DD>\n')
805		self.index('fn', name)
806
807	end_deftypefn = end_deffn
808
809	def bgn_deftypefun(self, args): self.bgn_deftypefn('Function ' + args)
810	end_deftypefun = end_deftypefn
811
812	def bgn_deftypevr(self, args):
813		words = splitwords(args, 3)
814		[category, datatype, name], rest = words[:3], words[3:]
815		self.write('<DL><DT>')
816		self.expand('@code{' + datatype + '} @b{' + name + '}')
817		# If there are too many arguments, show them
818		for word in rest: self.expand(' ' + word)
819		self.expand(' -- ' + category)
820		self.write('<DD>\n')
821		self.index('fn', name)
822
823	end_deftypevr = end_deftypefn
824
825	def bgn_deftypevar(self, args):
826		self.bgn_deftypevr('Variable ' + args)
827	end_deftypevar = end_deftypevr
828
829	# --- Ditto for object-oriented languages ---
830
831	def bgn_defcv(self, args):
832		words = splitwords(args, 3)
833		[category, classname, name], rest = words[:3], words[3:]
834		self.write('<DL><DT>')
835		self.expand('@b{' + name + '}')
836		# If there are too many arguments, show them
837		for word in rest: self.expand(' ' + word)
838		self.expand(' -- ' + category + ' of ' + classname)
839		self.write('<DD>\n')
840		self.index('vr', name + ' @r{of ' + classname + '}')
841
842	end_defcv = end_deftypevr
843
844	def bgn_defivar(self, args):
845		self.bgn_defcv('{Instance Variable} ' + args)
846	end_defivar = end_defcv
847
848	def bgn_defop(self, args):
849		self.write('<DL><DT>')
850		words = splitwords(args, 3)
851		[category, classname, name], rest = words[:3], words[3:]
852		self.expand('@b{' + name + '}')
853		for word in rest: self.expand(' ' + makevar(word))
854		self.expand(' -- ' + category + ' on ' + classname)
855		self.write('<DD>\n')
856		self.index('fn', name + ' @r{on ' + classname + '}')
857
858	end_defop = end_defcv
859
860	def bgn_defmethod(self, args):
861		self.bgn_defop('Method ' + args)
862	end_defmethod = end_defop
863
864	# --- Ditto for data types ---
865
866	def bgn_deftp(self, args):
867		self.write('<DL><DT>')
868		words = splitwords(args, 2)
869		[category, name], rest = words[:2], words[2:]
870		self.expand('@b{' + name + '}')
871		for word in rest: self.expand(' ' + word)
872		self.expand(' -- ' + category)
873		self.write('<DD>\n')
874		self.index('tp', name)
875
876	end_deftp = end_defcv
877
878	# --- Making Lists and Tables
879
880	def bgn_enumerate(self, args):
881		if not args: args = '1'
882		self.itemnumber = args
883		self.write('<UL>\n')
884	def end_enumerate(self):
885		self.itemnumber = None
886		self.write('</UL>\n')
887
888	def bgn_itemize(self, args):
889		self.itemarg = args
890		self.write('<UL>\n')
891	def end_itemize(self):
892		self.itemarg = None
893		self.write('</UL>\n')
894
895	def bgn_table(self, args):
896		self.itemarg = args
897		self.write('<DL>\n')
898	def end_table(self):
899		self.itemarg = None
900		self.write('</DL>\n')
901
902	def bgn_ftable(self, args):
903		self.itemindex = 'fn'
904		self.bgn_table(args)
905	def end_ftable(self):
906		self.itemindex = None
907		self.end_table()
908
909	def do_item(self, args):
910		if self.itemindex: self.index(self.itemindex, args)
911		if self.itemarg:
912			if self.itemarg[0] == '@' and self.itemarg[1:2] and \
913					self.itemarg[1] in string.letters:
914				args = self.itemarg + '{' + args + '}'
915			else:
916				# some other character, e.g. '-'
917				args = self.itemarg + ' ' + args
918		if self.itemnumber <> None:
919			args = self.itemnumber + '. ' + args
920			self.itemnumber = increment(self.itemnumber)
921		if self.stack and self.stack[-1] == 'table':
922			self.write('<DT>')
923			self.expand(args)
924			self.write('<DD>')
925		else:
926			self.write('<LI>')
927			self.expand(args)
928			self.write('  ')
929	do_itemx = do_item # XXX Should suppress leading blank line
930
931	# --- Enumerations, displays, quotations ---
932	# XXX Most of these should increase the indentation somehow
933
934	def bgn_quotation(self, args): self.write('<P>')
935	def end_quotation(self): self.write('<P>\n')
936
937	def bgn_example(self, args):
938		self.nofill = self.nofill + 1
939		self.write('<UL COMPACT><CODE>')
940	def end_example(self):
941		self.write('</CODE></UL>\n')
942		self.nofill = self.nofill - 1
943
944	bgn_lisp = bgn_example # Synonym when contents are executable lisp code
945	end_lisp = end_example
946
947	bgn_smallexample = bgn_example # XXX Should use smaller font
948	end_smallexample = end_example
949
950	bgn_smalllisp = bgn_lisp # Ditto
951	end_smalllisp = end_lisp
952
953	def bgn_display(self, args):
954		self.nofill = self.nofill + 1
955		self.write('<UL COMPACT>\n')
956	def end_display(self):
957		self.write('</UL>\n')
958		self.nofill = self.nofill - 1
959
960	def bgn_format(self, args):
961		self.nofill = self.nofill + 1
962		self.write('<UL COMPACT>\n')
963	def end_format(self):
964		self.write('</UL>\n')
965		self.nofill = self.nofill - 1
966
967	def do_exdent(self, args): self.expand(args + '\n')
968	# XXX Should really mess with indentation
969
970	def bgn_flushleft(self, args):
971		self.nofill = self.nofill + 1
972		self.write('<UL COMPACT>\n')
973	def end_flushleft(self):
974		self.write('</UL>\n')
975		self.nofill = self.nofill - 1
976
977	def bgn_flushright(self, args):
978		self.nofill = self.nofill + 1
979		self.write('<ADDRESS COMPACT>\n')
980	def end_flushright(self):
981		self.write('</ADDRESS>\n')
982		self.nofill = self.nofill - 1
983
984	def bgn_menu(self, args): self.write('<H2>Menu</H2><DL COMPACT>\n')
985	def end_menu(self): self.write('</DL>\n')
986
987	def bgn_cartouche(self, args): pass
988	def end_cartouche(self): pass
989
990	# --- Indices ---
991
992	def resetindex(self):
993		self.noncodeindices = ['cp']
994		self.indextitle = {}
995		self.indextitle['cp'] = 'Concept'
996		self.indextitle['fn'] = 'Function'
997		self.indextitle['ky'] = 'Keyword'
998		self.indextitle['pg'] = 'Program'
999		self.indextitle['tp'] = 'Type'
1000		self.indextitle['vr'] = 'Variable'
1001		#
1002		self.whichindex = {}
1003		for name in self.indextitle.keys():
1004			self.whichindex[name] = []
1005
1006	def user_index(self, name, args):
1007		if self.whichindex.has_key(name):
1008			self.index(name, args)
1009		else:
1010			print '*** No index named', `name`
1011
1012	def do_cindex(self, args): self.index('cp', args)
1013	def do_findex(self, args): self.index('fn', args)
1014	def do_kindex(self, args): self.index('ky', args)
1015	def do_pindex(self, args): self.index('pg', args)
1016	def do_tindex(self, args): self.index('tp', args)
1017	def do_vindex(self, args): self.index('vr', args)
1018
1019	def index(self, name, args):
1020		self.whichindex[name].append(args, self.nodename)
1021
1022	def do_synindex(self, args):
1023		words = string.split(args)
1024		if len(words) <> 2:
1025			print '*** bad @synindex', args
1026			return
1027		[old, new] = words
1028		if not self.whichindex.has_key(old) or \
1029			  not self.whichindex.has_key(new):
1030			print '*** bad key(s) in @synindex', args
1031			return
1032		if old <> new and \
1033			  self.whichindex[old] is not self.whichindex[new]:
1034			inew = self.whichindex[new]
1035			inew[len(inew):] = self.whichindex[old]
1036			self.whichindex[old] = inew
1037	do_syncodeindex = do_synindex # XXX Should use code font
1038
1039	def do_printindex(self, args):
1040		words = string.split(args)
1041		for name in words:
1042			if self.whichindex.has_key(name):
1043				self.prindex(name)
1044			else:
1045				print '*** No index named', `name`
1046
1047	def prindex(self, name):
1048		iscodeindex = (name not in self.noncodeindices)
1049		index = self.whichindex[name]
1050		if not index: return
1051		if self.debugging:
1052			print '--- Generating', self.indextitle[name], 'index'
1053		#  The node already provides a title
1054		index1 = []
1055		junkprog = regex.compile('^\(@[a-z]+\)?{')
1056		for key, node in index:
1057			sortkey = string.lower(key)
1058			# Remove leading `@cmd{' from sort key
1059			# -- don't bother about the matching `}'
1060			oldsortkey = sortkey
1061			while 1:
1062				i = junkprog.match(sortkey)
1063				if i < 0: break
1064				sortkey = sortkey[i:]
1065			index1.append(sortkey, key, node)
1066		del index[:]
1067		index1.sort()
1068		self.write('<DL COMPACT>\n')
1069		for sortkey, key, node in index1:
1070			if self.debugging > 1: print key, ':', node
1071			self.write('<DT>')
1072			if iscodeindex: key = '@code{' + key + '}'
1073			self.expand(key)
1074			self.write('<DD><A HREF="', makefile(node), \
1075				   '">', node, '</A>\n')
1076		self.write('</DL>\n')
1077
1078	# --- Final error reports ---
1079
1080	def report(self):
1081		if self.unknown:
1082			print '--- Unrecognized commands ---'
1083			cmds = self.unknown.keys()
1084			cmds.sort()
1085			for cmd in cmds:
1086				print string.ljust(cmd, 20), self.unknown[cmd]
1087
1088
1089# Put @var{} around alphabetic substrings
1090def makevar(str):
1091	# XXX This breaks if str contains @word{...}
1092	return regsub.gsub('\([a-zA-Z_][a-zA-Z0-9_]*\)', '@var{\\1}', str)
1093
1094
1095# Split a string in "words" according to findwordend
1096def splitwords(str, minlength):
1097	words = []
1098	i = 0
1099	n = len(str)
1100	while i < n:
1101		while i < n and str[i] in ' \t\n': i = i+1
1102		if i >= n: break
1103		start = i
1104		i = findwordend(str, i, n)
1105		words.append(str[start:i])
1106	while len(words) < minlength: words.append('')
1107	return words
1108
1109
1110# Find the end of a "word", matching braces and interpreting @@ @{ @}
1111fwprog = regex.compile('[@{} ]')
1112def findwordend(str, i, n):
1113	level = 0
1114	while i < n:
1115		i = fwprog.search(str, i)
1116		if i < 0: break
1117		c = str[i]; i = i+1
1118		if c == '@': i = i+1 # Next character is not special
1119		elif c == '{': level = level+1
1120		elif c == '}': level = level-1
1121		elif c == ' ' and level <= 0: return i-1
1122	return n
1123
1124
1125# Convert a node name into a file name
1126def makefile(nodename):
1127	return string.lower(fixfunnychars(nodename)) + '.html'
1128
1129
1130# Characters that are perfectly safe in filenames and hyperlinks
1131goodchars = string.letters + string.digits + '!@-_=+.'
1132
1133# Replace characters that aren't perfectly safe by underscores
1134def fixfunnychars(addr):
1135	i = 0
1136	while i < len(addr):
1137		c = addr[i]
1138		if c not in goodchars:
1139			c = '_'
1140			addr = addr[:i] + c + addr[i+1:]
1141		i = i + len(c)
1142	return addr
1143
1144
1145# Increment a string used as an enumeration
1146def increment(s):
1147	if not s:
1148		return '1'
1149	for sequence in string.digits, string.lowercase, string.uppercase:
1150		lastc = s[-1]
1151		if lastc in sequence:
1152			i = string.index(sequence, lastc) + 1
1153			if i >= len(sequence):
1154				if len(s) == 1:
1155					s = sequence[0]*2
1156					if s == '00':
1157						s = '10'
1158				else:
1159					s = increment(s[:-1]) + sequence[0]
1160			else:
1161				s = s[:-1] + sequence[i]
1162			return s
1163	return s # Don't increment
1164
1165
1166def test():
1167	import sys
1168	parser = TexinfoParser()
1169	while sys.argv[1:2] == ['-d']:
1170		parser.debugging = parser.debugging + 1
1171		del sys.argv[1:2]
1172	if len(sys.argv) <> 3:
1173		print 'usage: texi2html [-d] [-d] inputfile outputdirectory'
1174		sys.exit(2)
1175	file = sys.argv[1]
1176	parser.setdirname(sys.argv[2])
1177	if file == '-':
1178		fp = sys.stdin
1179	else:
1180		parser.setincludedir(os.path.dirname(file))
1181		try:
1182			fp = open(file, 'r')
1183		except IOError, msg:
1184			print file, ':', msg
1185			sys.exit(1)
1186	parser.parse(fp)
1187	fp.close()
1188	parser.report()
1189
1190
1191test()
1192