########################################################################
# $Header: /var/local/cvsroot/4Suite/Ft/Lib/DistExt/BuildDocs.py,v 1.46 2005/11/15 06:45:26 jkloth Exp $
"""
Main distutils extensions for generating documentation

Copyright 2005 Fourthought, Inc. (USA).
Detailed license and copyright information: http://4suite.org/COPYRIGHT
Project home, documentation, distributions: http://4suite.org/
"""

# NOTE: Before anybody gets wild ideas about changing urllib stuff to
# Ft.Lib.Uri, remember that this code is used *BEFORE* Ft is installed.
# It also is "safe" to urllib stuff as it will only be dealing with local
# files and its filename <-> url conversion works within itself.

import sys, os, posixpath, urllib
from distutils.core import Command
from distutils.dep_util import newer_group
from distutils.errors import *
from distutils.util import convert_path

import Structures

class BuildDocs(Command):

    command_name = 'build_docs'

    description = "build documentation files (copy or generate XML sources)"

    user_options = [
        ('build-dir=', 'd',
         "directory to \"build\" (generate) to"),
        ('force', 'f',
         "forcibly build everything (ignore file timestamps)"),
        ]

    boolean_options = ['force']

    def initialize_options(self):
        self.build_dir = None
        self.build_lib = None
        self.build_temp = None
        self.debug = None
        self.force = None

        self.build_xml = None
        self.build_html = None
        self.build_man = None

        # Not user selectable, needed for bdist_* commands
        self.xml_dir = None
        self.html_dir = None
        self.man_dir = None

        # The various files marked as documentation
        self.files = None       # 'raw' files, copied as-is to dest dir
        self.documents = None   # rendered documents
        self.modules = None     # meta-document for module API
        self.extensions = None  # meta-document for XPath/XSLT ext mods
        self.stylesheets = None # 

        self.outfiles = None
        return

    def finalize_options(self):
        self.set_undefined_options('build',
                                   ('build_docs', 'build_dir'),
                                   ('build_lib', 'build_lib'),
                                   ('build_temp', 'build_temp'),
                                   ('debug','debug'),
                                   ('force', 'force'))

        self.xml_dir = os.path.join(self.build_dir, 'xml')
        self.html_dir = os.path.join(self.build_dir, 'html')

        # Split apart the doc_files list into the various file types
        allfiles = self.distribution.doc_files or []

        # Plain files the are simply copied to docdir
        self.files = [ f for f in allfiles
                       if isinstance(f, Structures.File) ]
        
        # Files that need to be rendered with XSLT
        self.documents = [ f for f in allfiles
                           if isinstance(f, Structures.Document) ]

        self.modules = [ f for f in allfiles
                         if isinstance(f, Structures.ModulesDocument)]

        self.extensions = [ f for f in allfiles
                            if isinstance(f, Structures.ExtensionsDocument)]

        # Stylesheets used for rendering the above documents
        self.stylesheets = [ f for f in allfiles
                             if isinstance(f, Structures.Stylesheet) ]
        names = {}
        for sheet in self.stylesheets:
            if sheet.name in names:
                raise DistutilsSetupError(
                    "duplicate stylesheet '%s'" % sheet.name)
            names[sheet.name] = sheet
        return

    def get_outputs(self):
        if self.outfiles is None:
            self.dry_run = True
            self.run()
            del self.dry_run
        return self.outfiles

    def get_source_files(self):
        # Start with all the "raw" files
        filenames = [ f.source for f in self.files ]

        # Add the documents and their dependencies
        for docfile in self.documents:
            source = docfile.source
            includes = FindIncludes(source)
            filenames.append(source)
            filenames.extend(includes)
        return filenames

    def _get_outputs(self, has_func, build_cmd):
        if not has_func():
            return []
        # make sure sources are up to date
        self.run_command(build_cmd)
        # get the command object
        build_cmd = self.get_finalized_command(build_cmd)
        # strip off the build directory
        build_dir = build_cmd.build_lib
        prefix_len = len(build_dir) + len(os.sep)
        return [ outfile[prefix_len:] for outfile in build_cmd.get_outputs() ]

    def _mkstemp(self, suffix=''):
        # OK, not really temporary, but thats fine its usage here.
        return os.path.join(self.build_temp, self.get_command_name() + suffix)
        
    def _copy_document(self, source, destdir, copied=None):
        if copied is None:
            copied = {}
        def copy_includes(fullpath, path):
            if fullpath not in copied:
                # copied from parent scope
                copied[fullpath] = True
                # destdir from parent scope
                dest = os.path.join(destdir, path)
                dest = os.path.normpath(dest)
                self._copy_document(fullpath, os.path.dirname(dest), copied)
            return
        self.mkpath(destdir)
        destfile, _ = self.copy_file(source, destdir)
        self.outfiles.append(destfile)
        ProcessIncludes(source, copy_includes)
        return

    def run(self):
        # Most all of the work is done in an external process to allow
        # importing of all modules

        if os.name == 'nt' and self.debug:
            # On Windows, the logic for documenting ext modules needs
            # to learn about '_d' suffix on debug builds
            self.warn("unable to document using Win32 debug build")
            return

        self.outfiles = []

        # Add the documents and their dependencies
        for doc in self.documents:
            destdir = os.path.join(self.xml_dir, convert_path(doc.outdir))
            self._copy_document(convert_path(doc.source), destdir)

        # The commandline applications
        scripts = [ script for script in (self.distribution.scripts or [])
                    if isinstance(script, Structures.Script) ]

        # Locate all installed modules
        package_modules = {}
        build_py = self.get_finalized_command('build_py')
        for package, module, filename in build_py.find_all_modules():
            modules = package_modules.setdefault(package, [])
            modules.append((module, filename))
        build_ext = self.get_finalized_command('build_ext')
        for ext in self.distribution.ext_modules:
            fullname = build_ext.get_ext_fullname(ext.name)
            path = fullname.split('.')
            package = '.'.join(path[:-1])
            module = path[-1]
            filename = build_ext.get_ext_filename(ext.name)
            modules = package_modules.setdefault(package, [])
            modules.append((module, filename))
            
        # Create a script to do the documenting in a sub-process
        script_name = self._mkstemp('.py')
        outputs_name = self._mkstemp('.out')
        script = open(script_name, 'w')

        script.write('import sys\n')
        script.write('sys.path.insert(0, %r)\n' % self.build_lib)

        from Ft import __config__
        script.write('from Ft import __config__\n')
        script.write('__config__.DATADIR = %r\n' % __config__.DATADIR)

        script.write('from distutils.dist import Distribution\n')
        script.write('from distutils.errors import *\n')
        script.write('from distutils.util import grok_environment_error\n')
        script.write('from Ft.Lib.DistExt.Structures import *\n')
        script.write('from %s import ExternalBuildDocs\n' % (__name__))
        script.write('\n')

        script.write('attrs = %r\n' % vars(self.distribution.metadata))
        data = ',\n    '.join(map(repr, self.stylesheets))
        script.write('stylesheets = [\n    %s]\n' % data)
        data = ',\n    '.join(map(repr, self.documents))
        script.write('documents = [\n    %s]\n' % data)
        data = ',\n    '.join(map(repr, self.modules))
        script.write('modules = [\n    %s]\n' % data)
        script.write('package_modules = %r\n' % package_modules)
        data = ',\n    '.join(map(repr, self.extensions))
        script.write('extensions = [\n    %s]\n' % data)
        data = ',\n    '.join(map(repr, scripts))
        script.write('scripts = [\n    %s]\n' % data)

        for name in ('build_lib', 'build_dir', 'xml_dir', 'html_dir',
                     'man_dir', 'force', 'verbose', 'dry_run'):
            script.write('%s = %r\n' % (name, getattr(self, name)))

        script.write('mtime = %r\n' % self.distribution.timestamp)
        script.write('\n')

        script.write('cmd = ExternalBuildDocs(Distribution(attrs), '
                     'stylesheets, documents, modules, package_modules, '
                     'extensions, scripts, build_lib, build_dir, xml_dir, '
                     'html_dir, man_dir, force, verbose, dry_run, mtime)\n')
        script.write('try:\n')
        script.write('    cmd.run()\n')
        script.write('except KeyboardInterrupt:\n')
        script.write('    raise SystemExit\n')
        script.write('except EnvironmentError, e:\n')
        script.write('    raise SystemExit(grok_environment_error(e))\n')
        script.write('except DistutilsError, e:\n')
        script.write('    raise SystemExit("error: %s" % e)\n')

        script.write('file = open(%r, "w")\n' % outputs_name)
        script.write('file.write(repr(cmd.get_outputs()))\n')
        script.write('file.close()\n')
        script.close()

        # Get the list of generated files
        args = [sys.executable, script_name]
        rc = os.spawnv(os.P_WAIT, sys.executable, args)
        if rc:
            raise DistutilsExecError("%s process exited with code %d" %
                                     (self.get_command_name(), rc))
        try:
            outputs = open(outputs_name).read()
        except Exception, e:
            raise DistutilsExecError('corrupted data from %s process: %s' %
                                     (self.get_command_name(), str(e)))
        else:
            self.outfiles.extend(eval(outputs))
        return


def FindIncludes(source, xslt=False, _includes=None):
    if _includes is None:
        _includes = {}

    def gather_includes(fullpath, path):
        if fullpath not in _includes:
            _includes[fullpath] = True
            FindIncludes(fullpath, xslt, _includes)
        return
    ProcessIncludes(source, gather_includes, xslt)

    return _includes
    

def ProcessIncludes(source, callback, xslt=False):
    from xml.sax import make_parser, SAXException, SAXNotRecognizedException
    from xml.sax.handler import ContentHandler, feature_namespaces, \
         feature_validation, feature_external_ges, feature_external_pes

    # defined as nested to keep things "import clean"
    class InclusionFilter(ContentHandler):

        XSLT_INCLUDES = [
            ("http://www.w3.org/1999/XSL/Transform", "import"),
            ("http://www.w3.org/1999/XSL/Transform", "include"),
            ]

        def _url2pathname(self, url):
            urltype, url = urllib.splittype(url)
            if urltype is None or urltype == 'file':
                host, path = urllib.splithost(url)
                path = urllib.url2pathname(path)
                return os.path.normpath(str(path))
            return None
        def startDocument(self):
            self._bases = [self._locator.getSystemId()]
            self._elements = [
                ("http://www.w3.org/2001/XInclude", "include"),
                ]
            if xslt:
                self._elements.extend(self.XSLT_INCLUDES)
        def startElementNS(self, expandedName, tagName, attrs):
            # Update xml:base stack
            xml_base = ("http://www.w3.org/XML/1998/namespace", "base")
            baseUri = attrs.get(xml_base, self._bases[-1])
            self._bases.append(baseUri)

            if expandedName in self._elements:
                try:
                    href = attrs[(None, 'href')]
                except KeyError:
                    # XInclude same document reference, nothing to do
                    return

                # Ignore XInclude's with parse='text'
                if attrs.get((None, 'parse'), 'xml') == 'text':
                    return

                fullurl = urllib.basejoin(baseUri, href)
                fullpath = self._url2pathname(fullurl)
                if fullpath is not None and os.path.isfile(fullpath):
                    path = self._url2pathname(href)
                    callback(fullpath, path)
        def endElementNS(self, expandedName, tagName):
            del self._bases[-1]
    # -- end InclusionFilter --

    try:
        parser = make_parser()
        parser.setFeature(feature_namespaces, True)
        # Attempt to disable all external entity resolution
        for feature in (feature_validation, feature_external_ges,
                        feature_external_pes):
            try:
                parser.setFeature(feature, False)
            except SAXNotRecognizedException:
                pass
    except SAXException, e:
        raise DistutilsModuleError(e.getMessage())

    handler = InclusionFilter()
    parser.setContentHandler(handler)
    parser.parse(urllib.pathname2url(source))
    return


# -- externally executed -----------------------------------------------------

import inspect, copy, warnings, new

class ExternalBuildDocs(Command):

    command_name = 'build_docs'

    def __init__(self, dist,
                 stylesheets, general, modules, package_modules, extensions,
                 scripts, build_lib, build_dir, xml_dir, html_dir, man_dir,
                 force, verbose, dry_run, packages_mtime):
        self.distribution = dist

        self.stylesheets = stylesheets
        self.general = general
        self.modules = modules
        self.package_modules = package_modules
        self.scripts = scripts
        self.extensions = extensions

        self.build_lib = build_lib
        self.build_dir = build_dir
        self.xml_dir = xml_dir
        self.html_dir = html_dir
        self.man_dir = man_dir

        self.force = force
        self.verbose = verbose
        self.packages_mtime = packages_mtime
        try:
            from distutils import log
        except ImportError:
            pass
        else:
            log.set_verbosity(self.verbose)
        self.dry_run = dry_run

        self.outfiles = []

        # it is safe to modify these instances directly as they are in a
        # different process.
        for sheet in self.stylesheets:
            if isinstance(sheet, Structures.TextStylesheet):
                sheet.output_dir = text_dir
            elif isinstance(sheet, Structures.HtmlStylesheet):
                sheet.output_dir = html_dir
            sheet.depends = list(FindIncludes(sheet.source, True))
            sheet.source = convert_path(sheet.source)
        return

    def get_outputs(self):
        return self.outfiles

    def run(self):
        docs = []
        # Handwritten XML content
        if self.general:
            general = self.doc_general()
            docs.extend(general)

        # Generated XML content for API reference
        if self.modules:
            modules = self.doc_modules()
            docs.extend(modules)

        # Generated XML content for XPath/XSLT extensions
        if self.extensions:
            extensions = self.doc_extensions()
            docs.extend(extensions)

        # Generated XML content for command-line applications
        if self.scripts:
            commandline = self.doc_scripts()
            docs.extend(commandline)

        docs = self.build_index(docs)
        try:
            from Ft.Xml.Xslt import Processor
            Processor.Processor()
        except:
            self.warn('skipping rendering (unable to get a processor)')
        else:
            self.render_docs(docs)
        return

    def doc_general(self):
        docs = []
        for doc in self.general:
            # Create a new Document instance reflecting the build locations
            doc = copy.copy(doc)
            source = os.path.basename(convert_path(doc.source))
            outdir = convert_path(doc.outdir)
            doc.source = os.path.join(self.xml_dir, outdir, source)
            doc.outdir = outdir
            docs.append(doc)        
        return docs

    def document(self, category, name, sources, object, formatter):
        # The dependencies for 'object' are the source of this command,
        # the source for the formatter and, of course, 'sources'.
        sourcefile = __file__
        if sourcefile[:-4] in ('.pyc', '.pyo'):
            sourcefile = sourcefile[:-4] + '.py'
        deps = [sourcefile, inspect.getsourcefile(formatter.__class__)]
        deps.extend(sources)

        xmlfile = os.path.join(self.xml_dir, category, name + '.xml')

        self.outfiles.append(xmlfile)
        self.mkpath(os.path.dirname(xmlfile))
        if self.force or newer_group(deps, xmlfile, 'ignore'):
            self.announce("documenting %s -> %s" % (name, xmlfile), 2)
            if not self.dry_run:
                xml = formatter.document(object, encoding='iso-8859-1')
                open(xmlfile, 'wb').write(xml)
        else:
            self.announce('not documenting %s (up-to-date)' % name, 1)

        # Add the XSLT generated versions to the document list
        # Even if the XML hasn't changed, the stylesheet might have.
        return Structures.Document(source=xmlfile,
                                   outdir=category,
                                   stylesheet=category,
                                   category=category)

    def doc_modules(self):
        documented = {}
        for mod in self.modules:
            mod.modules = []
            for package in mod.packages:
                modules = []
                for module, filename in self.package_modules[package]:
                    if module == '__init__':
                        fullname = package
                        modules.insert(0, package)
                    else:
                        if package:
                            fullname = package + '.' + module
                        else:
                            fullname = module
                        modules.append(fullname)
                    documented[fullname] = filename
                mod.modules.extend(modules)

        from Formatters import ApiFormatter
        formatter = ApiFormatter.ApiFormatter(self, documented)
        category = 'modules'

        docs = []
        for mod in self.modules:
            # Find the top-level package to be documented
            top_level = os.path.commonprefix(mod.modules)
            assert top_level, ("a single top-level package is required for " +
                               mod.__class__.__name__)
            warnings.filterwarnings('ignore', '', DeprecationWarning,
                                    top_level)
            for name in mod.modules:
                try:
                    module = __import__(name, {}, {}, ['*'])
                except ImportError, e:
                    self.warn('not documenting %s (%s)' % (name, e))
                    continue

                # The build tree source is required for C-extension modules and
                # is safe for pure-Python modules.
                sources = [documented[name]]
                doc = self.document(category, name, sources, module, formatter)
                if name == top_level:
                    # Only those documents with a title will be listed on the
                    # index page
                    doc.title = mod.title
                    doc.category = 'general'
                docs.append(doc)
        return docs

    def doc_extensions(self):
        """
        Create XML documentation for XPath/XSLT extensions
        """
        from Formatters import ExtensionFormatter
        formatter = ExtensionFormatter.ExtensionFormatter(self)
        category = 'extensions'

        docs = []
        for ext in self.extensions:
            # create a temporary module that will contain the combined
            # extension information
            extmod = new.module(ext.name)
            extmod.ExtNamespaces = namespaces = {}
            extmod.ExtFunctions = functions = {}
            extmod.ExtElements = elements = {}
            sources = []
            for name in ext.modules:
                try:
                    module = __import__(name, {}, {}, ['*'])
                except ImportError, e:
                    raise DistutilsFileError(
                        "could not import '%s': %s" % (name, e))
                else:
                    namespaces.update(getattr(module, 'ExtNamespaces', {}))
                    functions.update(getattr(module, 'ExtFunctions', {}))
                    elements.update(getattr(module, 'ExtElements', {}))
                    sources.append(module.__file__)
            doc = self.document(category, ext.name, sources, extmod, formatter)
            doc.title = ext.title
            docs.append(doc)
        return docs

    def doc_scripts(self):
        from Formatters import CommandLineFormatter
        formatter = CommandLineFormatter.CommandLineFormatter(self)
        category = 'commandline'

        docs = []
        for script in self.scripts:
            try:
                module = __import__(script.module, {}, {}, [script.identifier])
            except ImportError, e:
                raise DistutilsFileError(
                    "could not import '%s': %s" % (script.name, e))

            app = getattr(module, script.identifier)()

            sources = [module.__file__]
            for cmd_name, cmd in app.get_help_doc_info():
                classmod = __import__(cmd.__class__.__module__, {}, {}, ['*'])
                sources.append(classmod.__file__)

            doc = self.document(category, script.name, sources, app, formatter)
            doc.title = '%s - %s' % (script.name, app.description)
            docs.append(doc)
        return docs

    def render_docs(self, docs):
        for doc in docs:
            basename = os.path.basename(doc.source)
            splitname = os.path.splitext(basename)[0]
            source = os.path.join(self.xml_dir, doc.outdir, basename)

            # Find the stylesheet to render this document
            for sheet in self.stylesheets:
                if sheet.name == doc.stylesheet:
                    break
            else:
                self.warn('not rendering %s (no stylesheet named %r)' %
                          (source, doc.stylesheet))
                continue

            # Render it!
            stylesheets = [sheet.source]
            destdir = os.path.join(sheet.output_dir, doc.outdir)
            outfile = os.path.join(destdir, splitname + sheet.output_extension)
                    
            deps = [doc.source, sheet.source] + sheet.depends
            if self.force or newer_group(deps, outfile):
                self.render_one(outfile, doc.source, stylesheets, doc.params)
            else:
                self.announce('not rendering %s (up-to-date)' % outfile, 1)

            self.outfiles.append(outfile)
            for src in sheet.extra_files:
                dst, copied = self.copy_file(src, destdir)
                self.outfiles.append(dst)
        return

    def render_one(self, outfile, source, stylesheets, params):
        from Ft.Xml.Xslt import Processor
        from Ft.Xml import InputSource
        from Ft.Lib import Uri

        self.mkpath(os.path.dirname(outfile))

        self.announce("rendering %s -> %s" % (source, outfile), 2)

        if not self.dry_run:
            # Add the stylesheets
            processor = Processor.Processor()
            ignorePis = 1
            for file in stylesheets:
                file = convert_path(file)
                if os.path.exists(file):
                    #url = 'file:' + urllib.pathname2url(file)
                    url = Uri.OsPathToUri(file, attemptAbsolute=True)
                    try:
                        iSrc = InputSource.DefaultFactory.fromUri(url)
                        processor.appendStylesheet(iSrc)
                    except KeyboardInterrupt:
                        raise
                    except Exception, exc:
                        raise DistutilsFileError, \
                              "could not use stylesheet '%s': %s" % (file, exc)
                else:
                    ignorePis = 0

            # Process the source overridden
            stream = open(outfile, 'w')
            url = Uri.OsPathToUri(source, attemptAbsolute=True)

            topParams = {'name' : self.distribution.get_name(),
                         'version' : self.distribution.version,
                         'fullname' : self.distribution.get_fullname(),
                         'author' : self.distribution.author,
                         'author-email' : self.distribution.author_email,
                         }
            topParams.update(params)
            try:
                iSrc = InputSource.DefaultFactory.fromUri(url)
                processor.run(iSrc, ignorePis, topParams, outputStream=stream)
            except KeyboardInterrupt:
                stream.close()
                os.remove(outfile)
                raise
            except Exception, exc:
                stream.close()
                os.remove(outfile)
                raise DistutilsFileError, \
                      "could not render '%s': %s" % (outfile, exc)
            stream.write('\n')
            stream.close()
        return

    def build_index(self, docs):
        from Ft.Lib import Uri
        from Ft.Xml.Xslt.BuiltInExtElements import RESERVED_NAMESPACE

        from Formatters import XmlFormatter
        xmlstr = XmlFormatter.XmlRepr().escape
        urlstr = lambda url: '/'.join(map(Uri.PercentEncode, url.split('/')))

        # Gather the source files to determine whether or not to write
        # a new index document.  This file is included as it contains the
        # template content for the index.
        sources = [__file__]
        index = {}
        for doc in docs:
            if not doc.title:
                continue
            sources.append(doc.source)
            source = os.path.basename(doc.source)
            source = posixpath.join(doc.outdir, source)
            basename = os.path.splitext(source)[0]
            for sheet in self.stylesheets:
                # only add index information for those documents rendered
                # as HTML.
                if sheet.name == doc.stylesheet and \
                       isinstance(sheet, Structures.HtmlStylesheet):
                    output = basename + sheet.output_extension
                    category = index.setdefault(doc.category, [])
                    category.append({'title' : xmlstr(doc.title),
                                     'source' : urlstr(source),
                                     'output' : urlstr(output),
                                     })

        xmlfile = os.path.join(self.xml_dir, 'index.doc')

        params = {'number-sections' : True,
                  'ignore-namespaces' : [RESERVED_NAMESPACE],
                  }
        docs.append(Structures.Document(xmlfile, '', 'sdocbook_html',
                                        params=params))

        if self.force or newer_group(sources, xmlfile, 'ignore') or \
               self.packages_mtime > os.stat(xmlfile).st_mtime:
            self.announce("creating index -> %s" % xmlfile, 2)
        else:
            self.announce('not creating index (up-to-date)', 1)
            return docs

        sections = []
        for title, category, sort in (
            ('General', 'general', False),
            ('Modules', 'modules', True),
            ('XPath/XSLT Extensions', 'extensions', False),
            ('Command-line Applications', 'commandline', True)
            ):
            if category not in index:
                continue
            items = []
            L = index[category]
            if sort:
                L.sort(lambda a, b: cmp(a['title'], b['title']))
            for info in L:
                repl = {'title' : info['title'],
                        'url' : info['output'],
                        }
                items.append(INDEX_LISTITEM % repl)
            if items:
                # add the section if it contains any entries
                items = ''.join(items)
                repl = {'title' : xmlstr(title),
                        'category' : xmlstr(category),
                        'items' : items,
                        }
                sections.append(INDEX_SECTION % repl)
        sections = ''.join(sections)

        sources = []
        for category in index.values():
            for info in category:
                sources.append(INDEX_SOURCE % info)
        sources = ''.join(sources)

        repl = {'fullname' : xmlstr(self.distribution.metadata.get_fullname()),
                'sections' : sections,
                'namespace' : RESERVED_NAMESPACE,
                'sources' : sources,
                }
        index = INDEX_TEMPLATE % repl

        if not self.dry_run:
            f = open(xmlfile, 'wb')
            f.write(index)
            f.close()

        return docs

INDEX_TITLES = {
    'general' : 'General',
    'modules' : 'Modules',
    'extensions' : 'XPath/XSLT Extensions',
    'commandline' : 'Command-line Applications',
    }

INDEX_TEMPLATE = """<?xml version="1.0" encoding="iso-8859-1"?>
<article>
  <title>%(fullname)s Document Index</title>
  <section id="preamble">
    <title>Preamble</title>
    <para>Copyright 2001-2005 Fourthought, Inc., USA.  Please read the <ulink url="COPYRIGHT.html" type="generate">COPYRIGHT</ulink> file for terms of license.</para>

    <para>For release notes and news, see the <ulink url="http://4suite.org/">4Suite home page</ulink>.</para>

    <para>For complete installation instructions, see the following documents listed in the <link linkend="general">General</link> section below: Installing 4Suite on UNIX/Windows, and 4Suite CVS access.</para>

    <para>If you have questions not answered by the documents, first stop is the <ulink url="http://4suite.org/docs/FAQ.xml">FAQ</ulink>.  Next try the Akara pages on <ulink url="http://uche.ogbuji.net/tech/akara/pyxml/">Python/XML</ulink> and <ulink url="http://uche.ogbuji.net/tech/akara/4suite/">4Suite</ulink>.  Then check the <ulink url="http://lists.fourthought.com/pipermail/4suite/">mailing archives</ulink> of the 4Suite list or post there.  Or drop by on IRC (#4suite on <ulink url="http://freenode.net/irc_servers.shtml">irc.freenode.net</ulink>).</para>

   <para>People interested in participating in or closely following the development process of 4Suite can join the <ulink url="http://lists.fourthought.com/mailman/listinfo/4suite-dev">mailing list for core 4Suite developers</ulink>, which is open (as are its <ulink url="http://lists.fourthought.com/pipermail/4suite-dev/">archives</ulink>).</para>

  <para>These links and others are also provided on <ulink url="http://uche.ogbuji.net/tech/4Suite/">Uche Ogbuji's 4Suite page</ulink>.</para>
  </section> <!-- preamble -->

%(sections)s

  <f:sources xmlns:f="%(namespace)s">
%(sources)s
  </f:sources>

</article>
"""

INDEX_SECTION = """
  <section id="%(category)s">
    <title>%(title)s</title>
    <itemizedlist>
%(items)s
    </itemizedlist>
  </section> <!-- %(category)s -->
"""

INDEX_LISTITEM = """\
      <listitem>
        <ulink url="%(url)s" type="generate">%(title)s</ulink>
      </listitem>
"""

INDEX_SOURCE = """\
    <f:source>
      <f:title>%(title)s</f:title>
      <f:src>%(source)s</f:src>
      <f:dst>%(output)s</f:dst>
    </f:source>
"""
