Modules | Files | Inheritance Tree | Inheritance Graph | Name Index | Config
File: Synopsis/Formatter/HTML/Page.py
    1| # $Id: Page.py,v 1.16 2003/01/16 12:46:46 chalky Exp $
    2| #
    3| # This file is a part of Synopsis.
    4| # Copyright (C) 2000, 2001 Stephen Davies
    5| # Copyright (C) 2000, 2001 Stefan Seefeld
    6| #
    7| # Synopsis is free software; you can redistribute it and/or modify it
    8| # under the terms of the GNU General Public License as published by
    9| # the Free Software Foundation; either version 2 of the License, or
   10| # (at your option) any later version.
   11| #
   12| # This program is distributed in the hope that it will be useful,
   13| # but WITHOUT ANY WARRANTY; without even the implied warranty of
   14| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   15| # General Public License for more details.
   16| #
   17| # You should have received a copy of the GNU General Public License
   18| # along with this program; if not, write to the Free Software
   19| # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
   20| # 02111-1307, USA.
   21| #
   22| # $Log: Page.py,v $
   23| # Revision 1.16  2003/01/16 12:46:46  chalky
   24| # Renamed FilePages to FileSource, FileTree to FileListing. Added FileIndexer
   25| # (used to be part of FileTree) and FileDetails.
   26| #
   27| # Revision 1.15  2002/11/16 04:12:33  chalky
   28| # Added strategies for page formatting, and added one to allow template HTML
   29| # files to be used.
   30| #
   31| # Revision 1.14  2002/11/01 03:39:21  chalky
   32| # Cleaning up HTML after using 'htmltidy'
   33| #
   34| # Revision 1.13  2002/10/29 12:43:56  chalky
   35| # Added flexible TOC support to link to things other than ScopePages
   36| #
   37| # Revision 1.12  2002/07/19 14:26:33  chalky
   38| # Revert prefix in FileLayout but keep relative referencing elsewhere.
   39| #
   40| # Revision 1.10  2002/01/09 11:43:41  chalky
   41| # Inheritance pics
   42| #
   43| # Revision 1.9  2002/01/09 10:16:35  chalky
   44| # Centralized navigation, clicking links in (html) docs works.
   45| #
   46| # Revision 1.8  2001/11/09 15:35:04  chalky
   47| # GUI shows HTML pages. just. Source window also scrolls to correct line.
   48| #
   49| # Revision 1.7  2001/07/05 05:39:58  stefan
   50| # advanced a lot in the refactoring of the HTML module.
   51| # Page now is a truely polymorphic (abstract) class. Some derived classes
   52| # implement the 'filename()' method as a constant, some return a variable
   53| # dependent on what the current scope is...
   54| #
   55| # Revision 1.6  2001/07/05 02:08:35  uid20151
   56| # Changed the registration of pages to be part of a two-phase construction
   57| #
   58| # Revision 1.5  2001/06/28 07:22:18  stefan
   59| # more refactoring/cleanup in the HTML formatter
   60| #
   61| # Revision 1.4  2001/06/26 04:32:16  stefan
   62| # A whole slew of changes mostly to fix the HTML formatter's output generation,
   63| # i.e. to make the output more robust towards changes in the layout of files.
   64| #
   65| # the rpm script now works, i.e. it generates source and binary packages.
   66| #
   67| # Revision 1.3  2001/02/05 05:26:24  chalky
   68| # Graphs are separated. Misc changes
   69| #
   70| # Revision 1.2  2001/02/01 15:23:24  chalky
   71| # Copywritten brown paper bag edition.
   72| #
   73| #
   74| 
   75| """
   76| Page base class, contains base functionality and common interface for all Pages.
   77| """
   78| 
   79| import os.path, cStringIO
   80| from Synopsis.Core import Util
   81| 
   82| import core
   83| from core import config
   84| from Tags import *
   85| 
   86| class PageFormat:
   87|     """Default and base class for formatting a page layout. The PageFormat
   88|     class basically defines the HTML used at the start and end of the page.
   89|     The default creates an XHTML compliant header and footer with a proper
   90|     title, and link to the stylesheet."""
   91|     def __init__(self):
   92|         self.__stylesheet = config.stylesheet
   93|         self.__prefix = ''
   94| 
   95|     def set_prefix(self, prefix):
   96|         """Sets the prefix to use to correctly reference files in the document
   97|         root directory."""
   98|         self.__prefix = prefix
   99| 
  100|     def stylesheet(self):
  101|         """Returns the relative filename of the stylesheet to use. The
  102|         stylesheet specified in the user's config is copied into the output
  103|         directory. If this page is not in the same directory, the url returned
  104|         from this function will have the appropriate number of '..'s added."""
  105|         return self.__prefix + self.__stylesheet
  106| 
  107|     def prefix(self):
  108|         """Returns the prefix to use to correctly reference files in the
  109|         document root directory. This will only ever not be '' if you are using the
  110|         NestedFileLayout, in which case it will be '' or '../' or '../../' etc
  111|         as appropraite."""
  112|         return self.__prefix
  113| 
  114|     def page_header(self, os, title, body, headextra):
  115|         """Called to output the page header to the given output stream.
  116|         @param os a file-like object (use os.write())
  117|         @param title the title of this page
  118|         @param body the body tag, which may contain extra parameters such as
  119|         onLoad scripts, and may also be empty eg: for the frames index
  120|         @param headextra extra html to put in the head section, such as
  121|         scripts
  122|         """
  123|         os.write('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">')
  124|         os.write("<html>\n<head>\n")
  125|         os.write(entity('title','Synopsis - '+ title) + '\n')
  126|         ss = self.stylesheet()
  127|         if ss: os.write(solotag('link', type='text/css', rel='stylesheet', href=ss) + '\n')
  128|         os.write(headextra)
  129|         os.write("</head>\n%s\n"%body)
  130| 
  131|     def page_footer(self, os, body):
  132|         """Called to output the page footer to the given output stream.
  133|         @param os a file-like object (use os.write())
  134|         @param body the close body tag, which may be empty eg: for the frames
  135|         index
  136|         """
  137|         os.write("\n%s\n</html>\n"%body)
  138| 
  139| class TemplatePageFormat (PageFormat):
  140|     """PageFormat subclass that uses a template file to define the HTML header
  141|     and footer for each page."""
  142|     def __init__(self):
  143|         PageFormat.__init__(self)
  144|         self.__file = ''
  145|         self.__re_body = re.compile('<body(?P<params>([ \t\n]+[-a-zA-Z0-9]+=("[^"]*"|\'[^\']*\'|[^ \t\n>]*))*)>', re.I)
  146|         self.__re_closebody = re.compile('</body>', re.I)
  147|         self.__re_closehead = re.compile('</head>', re.I)
  148|         self.__title_tag = '@TITLE@'
  149|         self.__content_tag = '@CONTENT@'
  150|         if hasattr(config.obj, 'TemplatePageFormat'):
  151|             myconfig = config.obj.TemplatePageFormat
  152|             if hasattr(myconfig, 'file'):
  153|                self.__file = myconfig.file
  154|             if hasattr(myconfig, 'copy_files'):
  155|                for file in myconfig.copy_files:
  156|                    dest = os.path.join(config.basename, file[1])
  157|                    config.files.copyFile(file[0], dest)
  158|         self.load_file()
  159| 
  160|     def load_file(self):
  161|         """Loads and parses the template file"""
  162|         f = open(self.__file, 'rt')
  163|         text = f.read(1024*64) # arbitrary max limit of 64kb
  164|         f.close()
  165|         # Find the content tag
  166|         content_index = text.find(self.__content_tag)
  167|         if content_index == -1:
  168|             print "Fatal: content tag '%s' not found in template file!"%self.__content_tag
  169|             raise SystemError, "Content tag not found"
  170|         header = text[:content_index]
  171|         # Find the title (doesn't matter if not found)
  172|         self.__title_index = text.find(self.__title_tag)
  173|         if self.__title_index:
  174|             # Remove the title tag
  175|             header = header[:self.__title_index] + \
  176|                     header[self.__title_index+len(self.__title_tag):]
  177|         # Find the close head tag
  178|         mo = self.__re_closehead.search(header)
  179|         if mo: self.__headextra_index = mo.start()
  180|         else: self.__headextra_index = -1
  181|         # Find the body tag
  182|         mo = self.__re_body.search(header)
  183|         if not mo:
  184|             print "Fatal: body tag not found in template file!"
  185|             print "(if you are sure there is one, this may be a bug in Synopsis)"
  186|             raise SystemError, "Body tag not found"
  187|         if mo.group('params'): self.__body_params = mo.group('params')
  188|         else: self.__body_params = ''
  189|         self.__body_index = mo.start()
  190|         header = header[:mo.start()] + header[mo.end():]
  191|         # Store the header
  192|         self.__header = header
  193|         footer = text[content_index+len(self.__content_tag):]
  194|         # Find the close body tag
  195|         mo = self.__re_closebody.search(footer)
  196|         if not mo:
  197|             print "Fatal: close body tag not found in template file"
  198|             raise SystemError, "Close body tag not found"
  199|         self.__closebody_index = mo.start()
  200|         footer = footer[:mo.start()] + footer[mo.end():]
  201|         self.__footer = footer
  202| 
  203|     def write(self, os, text):
  204|         """Writes the text to the output stream, replaceing @PREFIX@ with the
  205|         prefix for this file"""
  206|         sections = string.split(text, '@PREFIX@')
  207|         os.write(string.join(sections, self.prefix()))
  208| 
  209|     def page_header(self, os, title, body, headextra):
  210|         """Formats the header using the template file"""
  211|         if not body: return PageFormat.page_header(self, os, title, body, headextra)
  212|         header = self.__header
  213|         index = 0
  214|         if self.__title_index != -1:
  215|             self.write(os, header[:self.__title_index])
  216|             self.write(os, title)
  217|             index = self.__title_index
  218|         if self.__headextra_index != -1:
  219|             self.write(os, header[index:self.__headextra_index])
  220|             self.write(os, headextra)
  221|             index = self.__headextra_index
  222|         self.write(os, header[index:self.__body_index])
  223|         if body:
  224|             if body[-1] == '>':
  225|                self.write(os, body[:-1]+self.__body_params+body[-1])
  226|          else:
  227|                # Hmmmm... Should not happen, perhaps use regex?
  228|                self.write(os, body)
  229|         self.write(os, header[self.__body_index:])
  230| 
  231|     def page_footer(self, os, body):
  232|         """Formats the footer using the template file"""
  233|         if not body: return PageFormat.page_footer(self, os, body)
  234|         footer = self.__footer
  235|         self.write(os, footer[:self.__closebody_index])
  236|         self.write(os, body)
  237|         self.write(os, footer[self.__closebody_index:])
  238| 
  239| class Page:
  240|     """Base class for a Page. The base class provides a common interface, and
  241|     also handles common operations such as opening the file, and delegating
  242|     the page formatting to a strategy class.
  243|     @see PageFormat"""
  244|     def __init__(self, manager):
  245|         """Constructor, loads the formatting class.
  246|         @see PageFormat"""
  247|         self.manager = manager
  248|         self.__os = None
  249|         format_class = PageFormat
  250|         if config.page_format:
  251|             format_class = Util.import_object(config.page_format, basePackage = 'Synopsis.Formatter.HTML.Page.')
  252|         self.__format = format_class()
  253| 
  254|     def filename(self):
  255|         "Polymorphic method returning the filename associated with the page"
  256|         return ''
  257|     def title(self):
  258|         "Polymorphic method returning the title associated with the page"
  259|         return ''
  260| 
  261|     def os(self):
  262|         "Returns the output stream opened with start_file"
  263|         return self.__os
  264| 
  265|     def write(self, str):
  266|         """Writes the given string to the currently opened file"""
  267|         self.__os.write(str)
  268| 
  269|     def register(self):
  270|         """Registers this Page class with the PageManager. This method is
  271|         abstract - derived Pages should implement it to call the appropriate
  272|         methods in PageManager if they need to. This method is called after
  273|         construction."""
  274|         pass
  275| 
  276|     def register_filenames(self, start):
  277|         """Registers filenames for each file this Page will generate, given
  278|         the starting Scope."""
  279|         pass
  280| 
  281|     def get_toc(self, start):
  282|         """Retrieves the TOC for this page. This method assumes that the page
  283|         generates info for the the whole AST, which could be the ScopePages,
  284|         the FilePages (source code) or the XRefPages (cross reference info).
  285|         The default implementation returns None. Start is the declaration to
  286|         start processing from, which could be the global namespace."""
  287|         pass
  288|        
  289|     def process(self, start):
  290|         """Process the given Scope recursively. This is the method which is
  291|         called to actually create the files, so you probably want to override
  292|         it ;)"""
  293|         pass
  294| 
  295|     def process_scope(self, scope):
  296|         """Process just the given scope"""
  297|         pass
  298| 
  299|     def open_file(self):
  300|         """Returns a new output stream. This template method is for internal
  301|         use only, but may be overriden in derived classes.
  302|         The default joins config.basename and self.filename()
  303|         and uses Util.open()"""
  304|         return Util.open(os.path.join(config.basename, self.filename()))
  305| 
  306|     def close_file(self):
  307|         """Closes the internal output stream. This template method is for
  308|         internal use only, but may be overriden in derived classes."""
  309|         self.__os.close()
  310|         self.__os = None
  311|         
  312|     def start_file(self, body='<body>', headextra=''):
  313|         """Start a new file with given filename, title and body. This method
  314|         opens a file for writing, and writes the html header crap at the top.
  315|         You must specify a title, which is prepended with the project name.
  316|         The body argument is optional, and it is preferred to use stylesheets
  317|         for that sort of stuff. You may want to put an onLoad handler in it
  318|         though in which case that's the place to do it. The opened file is
  319|         stored and can be accessed using the os() method."""
  320|         self.__os = self.open_file()
  321|         prefix = rel(self.filename(), '')
  322|         self.__format.set_prefix(prefix)
  323|         self.__format.page_header(self.__os, self.title(), body, headextra)
  324|     
  325|     def end_file(self, body='</body>'):
  326|         """Close the file using given close body tag. The default is
  327|         just a close body tag, but if you specify '' then nothing will be
  328|         written (useful for a frames page)"""
  329|         self.__format.page_footer(self.__os, body)
  330|         self.close_file()
  331| 
  332|     def reference(self, name, scope, label=None, **keys):
  333|         """Returns a reference to the given name. The name is a scoped name,
  334|         and the optional label is an alternative name to use as the link text.
  335|         The name is looked up in the TOC so the link may not be local. The
  336|         optional keys are appended as attributes to the A tag."""
  337|         if not label: label = anglebrackets(Util.ccolonName(name, scope))
  338|         entry = config.toc[name]
  339|         if entry: return apply(href, (rel(self.filename(), entry.link), label), keys)
  340|         return label or ''
  341| 
  342| 
  343| class BufferPage (Page):
  344|     """A page that writes to a string buffer."""
  345|     def _take_control(self):
  346|         self.open_file = lambda s=self: BufferPage.open_file(s)
  347|         self.close_file = lambda s=self: BufferPage.close_file(s)
  348|         self.get_buffer = lambda s=self: BufferPage.get_buffer(s)
  349|         
  350|     def open_file(self):
  351|         "Returns a new StringIO"
  352|         return cStringIO.StringIO()
  353| 
  354|     def close_file(self):
  355|         "Does nothing."
  356|         pass
  357| 
  358|     def get_buffer(self):
  359|         """Returns the page as a string, then deletes the internal buffer"""
  360|         page = self.os().getvalue()
  361|         # NOW we do the close
  362|         Page.close_file(self)
  363|         return page