#
# FILE            $Id: ManualPage.py,v 1.12 1998/02/04 18:18:18 dlarsson Exp $
#
# DESCRIPTION     Manual page abstraction.
#
# AUTHOR          SEISY/LKSB Daniel Larsson
#
# Permission to use, copy, modify, and distribute this software and its
# documentation for any purpose and without fee is hereby granted,
# provided that the above copyright notice appear in all copies and that
# both that copyright notice and this permission notice appear in
# supporting documentation, and that the name of ABB Industrial Systems
# not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior permission.
#
# ABB INDUSTRIAL SYSTEMS DISCLAIMS ALL WARRANTIES WITH REGARD TO
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
# FITNESS, IN NO EVENT SHALL ABB INDUSTRIAL SYSTEMS BE LIABLE
# FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
# Copyright (C) ABB Industrial Systems AB, 1996
# Unpublished work.  All Rights Reserved.
# 
# HISTORY:
# $Log: /Gendoc/ManualPage.py $
# 
# 2     98-04-01 14:12 Daniel
# Fixes from mcfletch.
# 
# 1     98-04-01 13:15 Daniel
# Revision 1.12  1998/02/04 18:18:18  dlarsson
# Fix from R. Friedrich; replacing of links fouled up if the length of the
# rendered link was less than the original link length.
#
# Revision 1.11  1998/01/19 11:41:09  dlarsson
# Cosmetic change
#
# Revision 1.10  1996/09/06 19:12:07  omfadmin
# Removed surrounding characters on markups.
#
# Revision 1.9  1996/09/06  09:54:57  omfadmin
# Replaced setext markup with structured text markup.
#
# Revision 1.8  1996/08/26  20:29:07  omfadmin
# Added support for numbered and nested lists.
#      "        for definition lists.
#
# Revision 1.7  1996/07/11  16:38:09  omfadmin
# Split render_hyperlink into two, internal links and setext links.
# Caused problems since it was use to run on code sections and variables
# were sometimes assumed to be setext links.
#
# Revision 1.6  1996/07/10  03:03:10  omfadmin
# - Added set_author() method.
# - Fixed bug when replacing internal hyperlinks.
# - Fixed problem with method sections beeing reused.
#
# Revision 1.5  1996/07/08  05:15:41  omfadmin
# Added top(), previous() and next() for navigation buttons
# (primarily for HTML, but other formatters can use it too).
#
# Revision 1.4  1996/06/24  09:54:16  omfadmin
# Added support for setext style hyperlinks.
#
# Revision 1.3  1996/06/21  00:06:46  omfadmin
# - Fixed the hyperlink regular expression. It matched too much when multiple
#   links occurred on the same line. ':' is now forbidden in links and link
#   text.
# - Converts hyperlinks not only in code, but in paragraphs too.
#
# Revision 1.2  1996/06/13  17:50:33  omfadmin
# Simplified implementing formatters somewhat by moving code to find
# markups and hyperlinks to here.
#
# Revision 1.1  1995/04/15  13:36:11  dlarsson
# Initial revision
#
#

__version__ = "$Revision: 2 $"

import regex
import docregex

error = 'ManualPage.error'

# Verbosity level
VERBOSE = 0

def message(string, level=1):
    if VERBOSE >= level:
	print string


#
# NOTE: The syntax for hyperlinks expressed here is *only* an
# internal syntax. You're not supposed to know about this.
#
# Regular expression for hyperlinks.
# The _hyperlink expression groups a match into the following
# parts:
#   1   'HREF='
#   2   The hyperlink
#   3   '::'
#   4   The hyperlink text
#   5   '/HREF'
#
# The syntax is similar to the HTML construct, but not identical to make
# the substitution process by the HTML formatter easier.
#                                       ____1____    ____2____  
hyperlink_regex = regex.compile('HREF=\\([^:]*\\)::\\([^:]*\\)/HREF')


# Simulate enum type
PARAGRAPH,	\
CODE,		\
UN_LIST,	\
OR_LIST,        \
DEF_LIST,       \
SECTION	= tuple(range(1, 7))

def convert(formatter, text):
    """Convert list item text."""
    if type(text) == type(''):
	return formatter.convert_text(text)
    else: # Assume it is a tuple (listtype, list)
	return text[0], map(lambda text, f=formatter:convert(f, text), text[1])
	    

class ManualPage:
    """Abstract representation of manual page.

    This class enables you to create an abstract representation
    of a manual page. A manual page consists of a number of sections,
    possibly nested, and each section contain some paragraphs, lists
    and/or code snippets. To format the manual page, you must pass
    it a formatter instance. Formatters translate the manual page
    into a concrete syntax, such as HTML, MIF or UNIX manual page
    format.
    """

    # Available markups
    MARKUPS = [ ('strong',      docregex.strong_regex),
		('quoted',      docregex.code_regex),
		('emphasis',    docregex.emph_regex) ]

    def __init__(self, name):
	"""Initialize object."""

	self.author = None

	# References to other manpages
	self.topp = None
	self.prev = None
	self.nxt = None

	# These members hold the document
	self._name_ = name
	self._doctree_ = None
	self._cursection_ = None
	self._references_ = {}

	# Current level is set while rendering the document.
	# The level is the depth of the section nesting.
	# The method 'cur_level()' can be used by formatters
	# to find out at what level they currently are.
	self._cur_level_ = 0

	# Mapping from segment type to rendering function
	self._render_ = {
	    PARAGRAPH: self._render_paragraph,
	    CODE:      self._render_code,
	    UN_LIST:   self._render_list,
	    OR_LIST:   self._render_list,
	    DEF_LIST:  self._render_deflist,
	    SECTION:   self._render_subsection
	    }

	# Maps docregex hyperlinks to URLs
	self.links = {}

#    def __repr__(self):
#	return 'ManualPage for '+self._name_

    def name(self):
	"Name of this manual page"
	return self._name_

    def title(self, title, *marker):
	"""Set document title.

	~marker~ can be used to add the	title to an index. The marker
	is sent untouched to subclasses."""

	# Format: (level, supersection, title string, parts, marker)
	self._doctree_ = (0, None, title, [], marker)
	self._cursection_ = self._doctree_

    def set_author(self, author):
	"""Set the author of the software module."""

	self.author = author

    def top(self, top):
	"""Set up link to top manpage.

	If ~top~ is None, no link is set up"""

	self.topp = top

    def previous(self, prev):
	"""Set up link to previous manpage.

	If ~prev~ is None, no link is set up"""

	self.prev = prev

    def next(self, next):
	"""Set up link to next manpage.

	If ~next~ is None, no link is set up"""

	self.nxt = next

    def cur_level(self):
	"""The current section level during rendering.
	
	Intended for formatters."""
	return self._cur_level_

    def section(self, section, super_section=None, *marker, **kw):
	"""Add a new section.
	
	Sections can be added under other
	sections by giving a section object.
	
	RETURNS
	Either a newly created or a found section object.
	"""
	# If no section is given, assume it is a top section, i.e put
	# it directly under the doc tree root.
	if not super_section:
	    super_section = self._doctree_

	# Is there already is a section with the same name under
	# 'super_section'?
	sect = None
	if not kw.has_key('search') or kw['search']:
	    sect = self._find_section_(section, super_section)

	if sect:
	    sect = sect[0][1]
	else:
	    # No section found at current level. Search one level
	    # up also before creating a new one. This might not
	    # be the most logical thing to do??
	    sect = self._find_section_(section, super_section[1])

	    if sect:
		sect = sect[0][1]
		super_section = super_section[1]
	    else:
		# Ok, create a new empty section. The format is:
		#   (section level, supersection, section title, list of contents)
		sect = (super_section[0]+1, super_section, section, [], marker)
		self._add_to_section_(super_section, SECTION, sect)

	self._cursection_ = sect
	return self._cursection_

    def paragraph(self, para, section=None):
	"""Add a new paragraph under 'section'.
	
	If no section is given, use the section returned from the most
	recent	call to 'section(...)'
	"""
	self._add_to_section_(section, PARAGRAPH, para)

    def code(self, code, section=None):
	"""Add a new code paragraph under 'section'.
	
	If no section is given, use the section returned from the most
	recent call to 'section(...)'
	"""
	self._add_to_section_(section, CODE, code)

    def list(self, listtype, list, section=None):
	"""Add a new list under 'section'.
	
	If no section is given, use the section returned from the most
	recent call to 'section(...)'
	"""
	self._add_to_section_(section, listtype, (listtype, list))

    def deflist(self, list, section=None):
	"""Add a new definition list under 'section'.
	
	If no section is given, use the section returned from the most
	recent call to 'section(...)'
	"""
	self._add_to_section_(section, DEF_LIST, list)

    def add_hyperlink(self, url, text):
	"""Add docregex link definition.

	~url~ is the URL, and ~text~ is the docregex link name."""

	self.links[text] = url


    def render(self, formatter):
	"""Render the manual page.
	
	The render function traverses the manual page and lets the
	formatter render each part.
	
	RETURNS
	The formatted manual page as a string.
	"""
	
	# Pick up data from the root
	lvl, none, title, children, marker = self._doctree_

	# Render the title
	formatter.render_title(self, title, marker)

	# Render navigation links
	formatter.render_navigation(self.topp, self.prev, self.nxt)

	# Then render all the children
	for child in children:
	    apply(self._render_[child[0]], (formatter,)+tuple(child[1:]))

	# Finalizing document
	return formatter.end()

    def reference(self, ref, text=None):
	"""Return a reference to 'ref' with the text 'text'."""
	if not text:
	    text = ref

	return 'HREF=%s::%s/HREF' % (ref, text)


    # ---  Private Methods  ---
    def _find_section_recursive_(self, section_name, section=None):
	"""Search recursively for a section 'section_name' starting
	at 'section'."""
		
	# If section is not given, start at root
	if section == None:
	    section = self._doctree_

	# Search for section at this level
	sect = self._find_section_(section_name, section)
	if sect:
	    return sect
	else:
	    # Loop through all subsections and search them recursively
	    for sect in filter(lambda s: s[0] == SECTION, section[3]):
		sect = self._find_section_recursive_(section_name, sect[1])
		if sect:
		    return sect
	return None

    def _find_section_(self, section, super_section):
	"""Search for 'section' under 'super_section'. Only sections
	at the level below 'super_section' is searched."""
	if super_section:
	    return filter(lambda s, new=section: s[0] == SECTION and s[1][2] == new, super_section[3])
	else:
	    return None


    def _add_to_section_(self, sect, *node):
	"""Add a new element under 'section'.		
	
	If no section is given, use the section returned from the most
	recent call to 'section(...)'
	"""
	if not sect:
	    sect = self._cursection_
	sect[3].append(node)

    def _render_subsection(self, formatter, args):
	"Render a subsection."
	lvl, super, sect, children, marker = args

	# If section has no children, skip it
	if not children:
	    return

	# Set current level. This information might be needed
	# when rendering children (e.g. indentation)
	self._cur_level_ = lvl

	# First, render this section
	message("--- emitting section: %s\n " % sect)
	formatter.render_section(self, lvl, sect, marker)

	# then render the section's children
	for child in children:
	    apply(self._render_[child[0]], (formatter,)+tuple(child[1:]))


    def _render_paragraph(self, formatter, para):
	# First, let the formatter massage the text, such as
	# escaping certain characters. We must do this before
	# translating the hyperlinks, since otherwise it gets
	# pretty difficult for the HTML formatter to distinguish
	# '<' characters used in hyperlinks and those used in
	# the text.
	try:
	    para = formatter.convert_text(para)
	except AttributeError:
	    # convert_text probably not implemented. Ignore.
	    pass

	# Create hyperlinks
	para = self._render_internal_hyperlink(formatter, para)
	para = self._render_docregex_hyperlink(formatter, para)

	# Make markup substitutions
	para = self._render_markup(formatter, para)

	# Ok, substitutions are done. Render the paragraph.
	message("--- emitting paragraph: %s\n " % para)
	formatter.render_paragraph(self, para)

    def _render_code(self, formatter, code):
	try:
	    code = formatter.convert_text(code)
	except AttributeError:
	    # convert_text probably not implemented. Ignore.
	    pass

	# Create hyperlinks
	code = self._render_internal_hyperlink(formatter, code)

	message("--- emitting code: %s\n " % code)
	formatter.render_code(self, code)

    def _render_list(self, formatter, list):
	try:
	    list = convert(formatter, list)
	except AttributeError:
	    # convert_text probably not implemented. Ignore.
	    pass
	formatter.render_list(self, list)

    def _render_deflist(self, formatter, listt):
	ltype, list = listt
	try:
	    for i in range(len(list)):
		list[i] = formatter.convert_text(list[i][0]) , formatter.convert_text(list[i][1])
	except AttributeError:
	    # convert_text probably not implemented. Ignore.
	    pass

	formatter.render_deflist(self, list)

    def _render_markup(self, formatter, text):
	# Translate docregex markup into formatter specific markup
	for markup, regexp in self.MARKUPS:
	    index = length = 0
	    while index != -1 and index + length < len(text):
		index = regexp.search(text, index+length)
		if index >= 0:

		    matched_text = regexp.group(0)
		    length = len(matched_text)

		    prefix = regexp.group(1)
		    marked_text = regexp.group(2)

		    # Ok, we found a markup. Ask formatter for a
		    # substitution, and proceed.
		    mtd_name = 'render_'+markup

		    message('--- Found markup %s for "%s"' % (markup, marked_text), 2)
		    
		    if hasattr(formatter, mtd_name):
			method = getattr(formatter, mtd_name)
			marked_text = method(marked_text)

		    # Append punctuation back to string.
		    # Ugh, nasty hack :-(
		    if regexp == docregex.code_regex:
			marked_text = marked_text + regexp.group(4)
		    else:
			marked_text = marked_text + regexp.group(3)
		    text = text[:index] + prefix + marked_text + text[index+length:]

	return text

    def _render_internal_hyperlink(self, formatter, text):
	# Search for hyperlinks, and ask the formatter how
	# to translate these.
	index = 0
	link_len = 0
	rendered_link = ''
	while 1:
	    index = hyperlink_regex.search(text, index+len(rendered_link))

	    if index == -1: break

	    link_len = len(hyperlink_regex.group(0))

	    # Group 1 is the link, group 2 the link text
	    link, link_text = hyperlink_regex.group(1), hyperlink_regex.group(2)

	    # If there is a section in this manual
	    # with the same name as the link, make
	    # it an internal link, otherwise, assume
	    # it is external.
	    try:
		if self._find_section_recursive_(link):
		    rendered_link = formatter.render_internal_link(link, link_text)
		else:
		    rendered_link = formatter.render_external_link(link, link_text)
	    except AttributeError, m:
		rendered_link = link_text

	    # Substitute the found hyperlink with the rendered one
	    text = text[:index] + rendered_link + text[index+link_len:]

	return text


    def _render_docregex_hyperlink(self, formatter, text):
	from docregex import hypertext_regex
	import regsub

	index = 0
	while index >= 0:
	    index = hypertext_regex.search(text, index)
	    if index == -1: break

	    link_len = len(hypertext_regex.group(0))

	    try:
		prefix, link_text, suffix = hypertext_regex.group(1,2,3)
		link = self.links[link_text]
		try:
		    rendered_link = formatter.render_external_link(link, link_text)
		except AttributeError, m:
		    rendered_link = link_text
	    except KeyError, key:
		# We found a string "text" which obviously wasn't
		# a link. Regard it as just quoted text.
		rendered_link = '"' + link_text + '"'
		

	    # Substitute the found hyperlink with the rendered one
	    text = text[:index] + \
		   prefix + rendered_link + suffix + \
		   text[index+link_len:]

	    # Move index past the found regex
	    if link_len > len(rendered_link):
		index = index + len(rendered_link)
	    else:
		index = index + len(rendered_link) - link_len

	return text


def test():
    from ManFormatter import ASCIIFormatter
    doc = ManualPage('Test')
    txt = "Hej hopp _Daniel_. This is ~italic~. This is **bold**."
    txt2 = "This_is_a_link_."
    doc.title('TEST')
    doc.paragraph(txt)
    doc.paragraph(txt2)
    
    form = ASCIIFormatter()
    print doc.render(form)

def test2():
    from ManFormatter import HTMLFormatter
    

if __name__ == '__main__':
    test()
