#
# $Header: /usr/local/cvsroot/pythondoc/doctree.py,v 1.2 1999/05/01 01:02:24 daniel Exp $
#
# Copyright (C) Daniel Larsson
# All Rights Reserved.
#
# See copyright notice in the file 'LICENSE.TXT', which should have accompanied
# this distribution.

"""Classes for building the documentation tree.

This module contains the classes used to build the documentation
tree.
"""

import types, string
import docregex

XML_HEAD = '<?xml version="1.0"?>\n\n'

# Copied verbatim from HTMLgen
def escape(text):
    """Converts the special characters '<', '>', and '&'.

    RFC 1866 specifies that these characters be represented
    in HTML as &lt; &gt; and &amp; respectively. In Python
    1.5 we use the new string.replace() function for speed.
    """
    from string import replace
    text = replace(text, '&', '&amp;') # must be done 1st
    text = replace(text, '<', '&lt;')
    text = replace(text, '>', '&gt;')
    return text


class SubscribeHelper:
    def __init__(self):
	self.__subscribers = []

    def add_subscriber(self, subscriber):
	self.__subscribers.append(subscriber)

    def subscribers(self):
	return self.__subscribers

    def invoke(self, method, *args, **kw):
	for sub in self.__subscribers:
	    m = eval('sub.%s' % method)
	    apply(m, args, kw)

class DocNode:
    """Base class for document tree nodes.

    The document tree contains the documentation information for
    python objects. From this tree, manuals may be generated in
    various formats (XML, HTML, ...).
    """

    def __init__(self, text='', tag=None, sort_order=None, **attrs):
	"""Creates a document node.

	A document node is a part of a document tree, much like
	a node in an XML tree. Every node has a tag, a set of
	attributes, and some text. The tag is either given as
	argument, or the class name of the DocNode subclass is
	used.

	Arguments:

	  text -- The text to put in this node.

    	  tag -- The tag of the node denotes the type of information
	         stored in the node.

	  sort_order -- Sorting order for children. Should contain a
	                list of tag names for sorting order.

          attrs -- Attributes to decorate the node with.
	"""
	self.__children = []

	if tag:
	    self.__tag = tag
	else:
	    self.__tag = self.__class__.__name__
	self.__attrs = attrs
	self.__parent = None
	self.sort_order = sort_order
	if text:
	    self.add_child(text)

    def lessthan(self, c1, c2):
	try:
	    tagvalue = self.sort_order.index(c1.tag())
	    otagvalue = self.sort_order.index(c2.tag())
	except:
	    tagvalue = c1.tag()
	    otagvalue = c2.tag()

	tagcmp = cmp(tagvalue, otagvalue)
	if tagcmp != 0:
	    return tagcmp < 0

	try:
	    idcmp = cmp(c1.attribute('id'), c2.attribute('id'))
	    if idcmp != 0:
		return idcmp < 0
	except KeyError:
	    pass

	return id(c1) < id(c2)

    def tag(self):
	"Returns the node's tag"
	return self.__tag

    def attribute(self, attr):
	"""Returns the value of the given attribute.

	Exceptions:

	  KeyError -- The given attribute does not exist
	"""
	return self.__attrs[attr]

    def attribute_ne(self, attr, default=None):
	"""Returns the value of the given attribute."""
	return self.__attrs.get(attr, default)

    def attributes(self):
	"""Returns the attributes of the node as a dictionary."""
	return self.__attrs

    def add_attribute(self, key, value):
	"""Adds an attribute to this node."""
	assert not self.__attrs.has_key(key) or self.__attrs[key] == value
	self.__attrs[key] = value

    def children(self, name=None, tag=None):
	"""Returns children nodes.

	Returns all the children or, if a name is given
	the children with the given name."""

	from utilities import id2name
	if not name and not tag:
	    return self.__children

	children = []
	for child in self.__children:
	    id = child.attribute_ne('id')
	    if (not name or id and (id == name or id2name(id) == name)) \
	       and (not tag or child.tag() == tag):
		children.append(child)
	return children

    def add_child(self, child, ix=None):
	"Adds a child node"
	if type(child) == types.StringType:
	    child = TextNode(child)
	if ix:
	    self.__children.insert(ix, child)
	else:
	    self.__children.append(child)
	child.set_parent(self)

    def add_child_sorted(self, child, after=None):
	"Adds a child sorted."
	if after:
	    ix = self.__children.index(after) + 1
	else:
	    ix = 0
	for i in range(ix, len(self.__children)):
	    if self.lessthan(child, self.__children[i]):
		self.add_child(child, i)
		return
	self.add_child(child)

    def remove_child(self, child):
	del self.__children[self.__children.index(child)]

    def parent(self):
	return self.__parent

    def set_parent(self, parent):
	self.__parent = parent

    def getattr(self, attrname):
	"""Searches for named child.

	A node with an attribute 'id' is called a named
	node. This method will return the first child
	having an 'id' attribute with value 'name'.

	If none is found, 'None' is returned.
	"""
	child = self.child(name=attrname)
	if not child:
	    raise AttributeError, attrname
	return child

    def child(self, name=None, tag=None):
	"""Searches for named child.

	A node with an attribute 'id' is called a named
	node. This method will return the first child
	having an 'id' attribute with value 'name'.

	If none is found, 'None' is returned.
	"""
	for child in self.__children:
	    if (not name or child.attribute_ne('id') == name) \
	       and (not tag or child.tag() == tag):
		return child
	return None

    def all(self, name=None, tag=None):
	"""Searches recursively for children with given name.

	'all' recursively looks for children with an attribute
	'id' having the value 'name'. A list of the matches is
	returned.
	"""
	result = self.children(name, tag)

	for child in self.__children:
	    result = result + child.all(name, tag)
	return result

    def traverse(self):
	method = "begin_" + string.lower(self.tag())
	for subscriber in self.subscribers():
	    try:
		try:
		    m = eval('subscriber.%s' % method)
		except AttributeError, attr:
		    if attr != method:
			import sys
			raise AttributeError, attr
		    m = subscriber.begin_node
		m(self)
	    except AttributeError, attr:
		pass

	for child in self.__children:
	    child.traverse()

	method = "end_" + string.lower(self.tag())
	for subscriber in self.subscribers():
	    try:
		try:
		    m = eval('subscriber.%s' % method)
		except AttributeError:
		    m = subscriber.end_node
		m(self)
	    except AttributeError, attr:
		pass


    def xml_source(self):
	return XML_HEAD + str(self)

    def __str__(self):
	if self.__attrs:
	    attrs = reduce(lambda c, a: c + ' %s="%s"' % a,
			   self.__attrs.items(), '')
	else:
	    attrs = ''
	childtext = reduce(lambda c, a: c + str(a), self.__children, '')
	if not (attrs or childtext):
	    return ''

	# If there's no children, let's use the XML shorthand notation
	if not childtext:
	    return '\n<%s%s/>\n' % (self.__tag, attrs)
	return '\n<%s%s>%s</%s>\n' % (self.__tag, attrs, childtext, self.__tag)

    def text(self):
	textchildren = self.children(tag="TextNode")
	if len(textchildren) == 1:
	    return textchildren[0].text()
	else:
	    raise ValueError, "no single text node"

class TextNode(DocNode):
    def __init__(self, text):
	DocNode.__init__(self)
	self.__text = text

    def __str__(self):
	return escape(self.__text)

    def text(self):
	return self.__text

    def traverse(self):
	for subscriber in self.subscribers():
	    try:
		subscriber.visit_text(self)
	    except AttributeError:
		pass


# DocNode subclasses
class Name(DocNode):
    "Contains the name of the documented object."
    pass

class Oneliner(DocNode):
    "Contains the oneliner docstring for an object."
    pass

class Arguments(DocNode):
    "Contains a list of 'Argument' nodes describing a function's arguments."

class Argument(DocNode):
    "Contains a description of a function's argument."
    pass

class Default(DocNode):
    "Contains the default value for an argument."

class Value(DocNode):
    "Contains the value of a variable."

class Description(DocNode):
    "Contains the descriptive text for an object."
    pass

class Heading(DocNode):
    "Contains a heading within a description"
    pass

class Title(DocNode):
    "Title of a section"
    pass

class P(DocNode):
    "Contains a paragraph of text within a description."
    pass

class UL(DocNode):
    pass

class OL(DocNode):
    pass

class DL(DocNode):
    pass

class LI(DocNode):
    pass

class Term(DocNode):
    pass

class Def(DocNode):
    pass

class Exceptions(DocNode):
    pass

class Inherited(DocNode):
    pass

class BaseClasses(DocNode):
    pass

class BaseClass(DocNode):
    pass

class Example(DocNode):
    pass

class Code(DocNode):
    pass

class PyCode(DocNode):
    pass

class Strong(DocNode):
    pass

class Emp(DocNode):
    pass

class A(DocNode):
    "Anchor"
    pass

class Usage(DocNode):
    "Usage section"
    pass

#
# $Log: doctree.py,v $
# Revision 1.2  1999/05/01 01:02:24  daniel
# Removed Windows style line endings.
#
# 
# *****************  Version 5  *****************
# User: Daniel       Date: 98-12-13   Time: 16:30
# Updated in $/Pythondoc
# Attribute 'path' is now removed (replaced with 'id').
# 
# *****************  Version 4  *****************
# User: Daniel       Date: 98-08-11   Time: 21:03
# Updated in $/Pythondoc
# - When search for children with specific name, the 'path' attribute is
# now also checked (and not only the 'id' attribute).
# - Added the PyCode and Usage classes
# 
# *****************  Version 3  *****************
# User: Daniel       Date: 98-08-06   Time: 17:18
# Updated in $/Pythondoc
# Added "non-exception throwing" version of 'attributes' (attributes_ne).
# Replaced some calls to 'attribute' with calls to 'attribute_ne' where
# an exception is undesireable.
# 
# *****************  Version 2  *****************
# User: Daniel       Date: 98-07-31   Time: 2:36
# Updated in $/Pythondoc
# Added searching for nodes with given tag in addition to given id.
# Minor bug fixes.
# 
