# Tools for info file processing.

# XXX Need to be more careful with reading ahead searching for nodes.


import regexp
import string


# Exported exceptions.
#
NoSuchFile = 'no such file'
NoSuchNode = 'no such node'


# The search path for info files; this is site-specific.
# Directory names should end in a partname delimiter,
# so they can simply be concatenated to a relative pathname.
#
#INFOPATH = ['', ':Info.Ibrowse:', ':Info:']	# Mac
INFOPATH = ['', '/usr/local/emacs/info/']	# X11 on UNIX


# Tunable constants.
#
BLOCKSIZE = 512			# Qty to align reads to, if possible
FUZZ = 2*BLOCKSIZE		# Qty to back-up before searching for a node
CHUNKSIZE = 4*BLOCKSIZE		# Qty to read at once when reading lots of data


# Regular expressions used.
# Note that it is essential that Python leaves unrecognized backslash
# escapes in a string so they can be seen by regexp.compile!
#
findheader = regexp.compile('\037\014?\n(.*\n)').match
findescape = regexp.compile('\037').match
parseheader = regexp.compile('[nN]ode:[ \t]*([^\t,\n]*)').match
findfirstline = regexp.compile('^.*\n').match
findnode = regexp.compile('[nN]ode:[ \t]*([^\t,\n]*)').match
findprev = regexp.compile('[pP]rev[ious]*:[ \t]*([^\t,\n]*)').match
findnext = regexp.compile('[nN]ext:[ \t]*([^\t,\n]*)').match
findup = regexp.compile('[uU]p:[ \t]*([^\t,\n]*)').match
findmenu = regexp.compile('^\* [mM]enu:').match
findmenuitem = regexp.compile( \
	'^\* ([^:]+):[ \t]*(:|\([^\t]*\)[^\t,\n.]*|[^:(][^\t,\n.]*)').match
findfootnote = regexp.compile( \
	'\*[nN]ote ([^:]+):[ \t]*(:|[^:][^\t,\n.]*)').match
parsenoderef = regexp.compile('^\((.*)\)(.*)$').match


# Get a node and all information pertaining to it.
# This doesn't work if there is an indirect tag table,
# and in general you are better off using icache.get_node() instead.
# Functions get_whole_file() and get_file_node() provide part
# functionality used by icache.
# Raise NoSuchFile or NoSuchNode as appropriate.
#
def get_node(curfile, ref):
	file, node = parse_ref(curfile, ref)
	if node == '*':
		return get_whole_file(file)
	else:
		return get_file_node(file, 0, node)
#
def get_whole_file(file):
	f = try_open(file) # May raise NoSuchFile
	text = f.read()
	header, menu, footnotes = ('', '', ''), [], []
	return file, '*', header, menu, footnotes, text
#
def get_file_node(file, offset, node):
	f = try_open(file) # May raise NoSuchFile
	text = find_node(f, offset, node) # May raise NoSuchNode
	node, header, menu, footnotes = analyze_node(text)
	return file, node, header, menu, footnotes, text


# Parse a node reference into a file (possibly default) and node name.
# Possible reference formats are: "NODE", "(FILE)", "(FILE)NODE".
# Default file is the curfile argument; default node is Top.
# A node value of '*' is a special case: the whole file should
# be interpreted (by the caller!) as a single node.
#
def parse_ref(curfile, ref):
	match = parsenoderef(ref)
	if not match:
		file, node = curfile, ref
	else:
		(a, b), (a1, b1), (a2, b2) = match
		file, node = ref[a1:b1], ref[a2:b2]
	if not file:
		file = curfile # (Is this necessary?)
	if not node:
		node = 'Top'
	return file, node


# Extract node name, links, menu and footnotes from the node text.
#
def analyze_node(text):
	#
	# Get node name and links from the header line
	#
	match = findfirstline(text)
	if match:
		(a, b) = match[0]
		line = text[a:b]
	else:
		line = ''
	node = get_it(text, findnode)
	prev = get_it(text, findprev)
	next = get_it(text, findnext)
	up = get_it(text, findup)
	#
	# Get the menu items, if there is a menu
	#
	menu = []
	match = findmenu(text)
	if match:
		(a, b) = match[0]
		while 1:
			match = findmenuitem(text, b)
			if not match:
				break
			(a, b), (a1, b1), (a2, b2) = match
			topic, ref = text[a1:b1], text[a2:b2]
			if ref == ':':
				ref = topic
			menu.append((topic, ref))
	#
	# Get the footnotes
	#
	footnotes = []
	b = 0
	while 1:
		match = findfootnote(text, b)
		if not match:
			break
		(a, b), (a1, b1), (a2, b2) = match
		topic, ref = text[a1:b1], text[a2:b2]
		if ref == ':':
			ref = topic
		footnotes.append((topic, ref))
	#
	return node, (prev, next, up), menu, footnotes
#
def get_it(line, matcher):
	match = matcher(line)
	if not match:
		return ''
	else:
		(a, b), (a1, b1) = match
		return line[a1:b1]


# Find a node in an open file.
# The offset (from the tags table) is a hint about the node's position.
# Pass zero if there is no tags table.
# Raise NoSuchNode if the node isn't found.
# NB: This seeks around in the file.
#
def find_node(f, offset, node):
	node = string.lower(node) # Just to be sure
	#
	# Position a little before the given offset,
	# so we may find the node even if it has moved around
	# in the file a little.
	#
	offset = max(0, ((offset-FUZZ) / BLOCKSIZE) * BLOCKSIZE)
	f.seek(offset)
	#
	# Loop, hunting for a matching node header.
	#
	while 1:
		buf = f.read(CHUNKSIZE)
		if not buf:
			break
		i = 0
		while 1:
			match = findheader(buf, i)
			if match:
				(a,b), (a1,b1) = match
				start = a1
				line = buf[a1:b1]
				i = b
				match = parseheader(line)
				if match:
					(a,b), (a1,b1) = match
					key = string.lower(line[a1:b1])
					if key == node:
						# Got it!  Now read the rest.
						return read_node(f, buf[start:])
			elif findescape(buf, i):
				next = f.read(CHUNKSIZE)
				if not next:
					break
				buf = buf + next
			else:
				break
	#
	# If we get here, we didn't find it.  Too bad.
	#
	raise NoSuchNode, node


# Finish off getting a node (subroutine for find_node()).
# The node begins at the start of buf and may end in buf;
# if it doesn't end there, read additional data from f.
#
def read_node(f, buf):
	i = 0
	match = findescape(buf, i)
	while not match:
		next = f.read(CHUNKSIZE)
		if not next:
			end = len(buf)
			break
		i = len(buf)
		buf = buf + next
		match = findescape(buf, i)
	else:
		# Got a match
		(a, b) = match[0]
		end = a
	# Strip trailing newlines
	while end > 0 and buf[end-1] == '\n':
		end = end-1
	buf = buf[:end]
	return buf


# Read reverse starting at offset until the beginning of a node is found.
# Then return a buffer containing the beginning of the node,
# with f positioned just after the buffer.
# The buffer will contain at least the full header line of the node;
# the caller should finish off with read_node() if it is the right node.
# (It is also possible that the buffer extends beyond the node!)
# Return an empty string if there is no node before the given offset.
#
def backup_node(f, offset):
	start = max(0, ((offset-CHUNKSIZE) / BLOCKSIZE) * BLOCKSIZE)
	end = offset
	while start < end:
		f.seek(start)
		buf = f.read(end-start)
		i = 0
		hit = -1
		while 1:
			match = findheader(buf, i)
			if match:
				(a,b), (a1,b1) = match
				hit = a1
				i = b
			elif end < offset and findescape(buf, i):
				next = f.read(min(offset-end, BLOCKSIZE))
				if not next:
					break
				buf = buf + next
				end = end + len(next)
			else:
				break
		if hit >= 0:
			return buf[hit:]
		end = start
		start = max(0, end - CHUNKSIZE)
	return ''


# Make a tag table for the given file by scanning the file.
# The file must be open for reading, and positioned at the beginning
# (or wherever the hunt for tags must begin; it is read till the end).
#
def make_tags(f):
	tags = {}
	while 1:
		offset = f.tell()
		buf = f.read(CHUNKSIZE)
		if not buf:
			break
		i = 0
		while 1:
			match = findheader(buf, i)
			if match:
				(a,b), (a1,b1) = match
				start = offset+a1
				line = buf[a1:b1]
				i = b
				match = parseheader(line)
				if match:
					(a,b), (a1,b1) = match
					key = string.lower(line[a1:b1])
					if tags.has_key(key):
						print 'Duplicate node:',
						print key
					tags[key] = '', start, line
			elif findescape(buf, i):
				next = f.read(CHUNKSIZE)
				if not next:
					break
				buf = buf + next
			else:
				break
	return tags


# Try to open a file, return a file object if succeeds.
# Raise NoSuchFile if the file can't be opened.
# Should treat absolute pathnames special.
#
def try_open(file):
	for dir in INFOPATH:
		try:
			return open(dir + file, 'r')
		except IOError:
			pass
	raise NoSuchFile, file


# A little test for the speed of make_tags().
#
TESTFILE = 'texinfo-1'
def test_make_tags():
	import time
	f = try_open(TESTFILE)
	t1 = time.millitimer()
	tags = make_tags(f)
	t2 = time.millitimer()
	print 'Making tag table for', `TESTFILE`, 'took', t2-t1, 'msec.'
