File: Synopsis/Formatters/HTML/View.py
  1#
  2# Copyright (C) 2000 Stephen Davies
  3# Copyright (C) 2000 Stefan Seefeld
  4# All rights reserved.
  5# Licensed to the public under the terms of the GNU LGPL (>= 2),
  6# see the file COPYING for details.
  7#
  8
  9"""
 10View base class, contains base functionality and common interface for all Views.
 11"""
 12
 13from Synopsis.Processor import Parametrized, Parameter
 14from Synopsis.Formatters import open_file
 15from Tags import *
 16
 17import re, os
 18
 19class Format(Parametrized):
 20    """Default and base class for formatting a view layout. The Format
 21    class basically defines the HTML used at the start and end of the view.
 22    The default creates an XHTML compliant header and footer with a proper
 23    title, and link to the stylesheet."""
 24
 25    def init(self, processor, prefix):
 26
 27        self.prefix = prefix
 28
 29    def view_header(self, os, title, body, headextra, view):
 30        """Called to output the view header to the given output stream.
 31        @param os a file-like object (use os.write())
 32        @param title the title of this view
 33        @param body the body tag, which may contain extra parameters such as
 34        onLoad scripts, and may also be empty eg: for the frames index
 35        @param headextra extra html to put in the head section, such as
 36        scripts
 37        """
 38
 39        os.write('<?xml version="1.0" encoding="iso-8859-1"?>\n')
 40        os.write('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"\n')
 41        os.write('    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
 42        os.write('<html xmlns="http://www.w3.org/1999/xhtml" lang="en">\n')
 43        os.write('<!-- ' + view.filename() + ' -->\n')
 44        os.write('<!-- this view was generated by ' + view.__class__.__name__ + ' -->\n')
 45        os.write("<head>\n")
 46        os.write('<meta content="text/html; charset=iso-8859-1" http-equiv="Content-Type"/>\n')
 47        os.write(element('title','Synopsis - '+ title) + '\n')
 48        css = self.prefix + 'synopsis.css'
 49        os.write(element('link', type='text/css', rel='stylesheet', href=css) + '\n')
 50        js = self.prefix + 'synopsis.js'
 51        os.write(element('script', '', type='text/javascript', src=js) + '\n')
 52        os.write(headextra)
 53        os.write("</head>\n%s\n"%body)
 54
 55    def view_footer(self, os, body):
 56        """Called to output the view footer to the given output stream.
 57        @param os a file-like object (use os.write())
 58        @param body the close body tag, which may be empty eg: for the frames
 59        index
 60        """
 61
 62        os.write("\n%s\n</html>\n"%body)
 63
 64class Template(Format):
 65    """Format subclass that uses a template file to define the HTML header
 66    and footer for each view."""
 67
 68    template = Parameter('', 'the html template file')
 69    copy_files = Parameter([], 'a list of files to be copied into the output dir')
 70
 71    def init(self, processor, prefix):
 72
 73        Format.init(self, processor, prefix)
 74        self.__re_body = re.compile('<body(?P<params>([ \t\n]+[-a-zA-Z0-9]+=("[^"]*"|\'[^\']*\'|[^ \t\n>]*))*)>', re.I)
 75        self.__re_closebody = re.compile('</body>', re.I)
 76        self.__re_closehead = re.compile('</head>', re.I)
 77        self.__title_tag = '@TITLE@'
 78        self.__content_tag = '@CONTENT@'
 79        for file in self.copy_files:
 80            processor.file_layout.copy_file(file, file)
 81        self.load_file()
 82
 83    def load_file(self):
 84        """Loads and parses the template file"""
 85
 86        f = open(self.template, 'rt')
 87        text = f.read(1024*64) # arbitrary max limit of 64kb
 88        f.close()
 89        # Find the content tag
 90        content_index = text.find(self.__content_tag)
 91        if content_index == -1:
 92            print "Fatal: content tag '%s' not found in template file!"%self.__content_tag
 93            raise SystemError, "Content tag not found"
 94        header = text[:content_index]
 95        # Find the title (doesn't matter if not found)
 96        self.__title_index = text.find(self.__title_tag)
 97        if self.__title_index != -1:
 98            # Remove the title tag
 99            header = header[:self.__title_index] +header[self.__title_index+len(self.__title_tag):]
100        # Find the close head tag
101        mo = self.__re_closehead.search(header)
102        if mo: self.__headextra_index = mo.start()
103        else: self.__headextra_index = -1
104        # Find the body tag
105        mo = self.__re_body.search(header)
106        if not mo:
107            print "Fatal: body tag not found in template file!"
108            print "(if you are sure there is one, this may be a bug in Synopsis)"
109            raise SystemError, "Body tag not found"
110        if mo.group('params'): self.__body_params = mo.group('params')
111        else: self.__body_params = ''
112        self.__body_index = mo.start()
113        header = header[:mo.start()] + header[mo.end():]
114        # Store the header
115        self.__header = header
116        footer = text[content_index+len(self.__content_tag):]
117        # Find the close body tag
118        mo = self.__re_closebody.search(footer)
119        if not mo:
120            print "Fatal: close body tag not found in template file"
121            raise SystemError, "Close body tag not found"
122        self.__closebody_index = mo.start()
123        footer = footer[:mo.start()] + footer[mo.end():]
124        self.__footer = footer
125
126    def write(self, os, text):
127        """Writes the text to the output stream, replaceing @PREFIX@ with the
128        prefix for this file"""
129
130        sections = text.split('@PREFIX@')
131        os.write(self.prefix.join(sections))
132
133    def view_header(self, os, title, body, headextra, view):
134        """Formats the header using the template file"""
135
136        if not body: return Format.view_header(self, os, title, body, headextra)
137        header = self.__header
138        index = 0
139        if self.__title_index != -1:
140            self.write(os, header[:self.__title_index])
141            self.write(os, title)
142            index = self.__title_index
143        if self.__headextra_index != -1:
144            self.write(os, header[index:self.__headextra_index])
145            self.write(os, headextra)
146            index = self.__headextra_index
147        self.write(os, header[index:self.__body_index])
148        if body:
149            if body[-1] == '>':
150                self.write(os, body[:-1]+self.__body_params+body[-1])
151            else:
152                # Hmmmm... Should not happen, perhaps use regex?
153                self.write(os, body)
154        self.write(os, header[self.__body_index:])
155
156    def view_footer(self, os, body):
157        """Formats the footer using the template file"""
158
159        if not body: return Format.view_footer(self, os, body)
160        footer = self.__footer
161        self.write(os, footer[:self.__closebody_index])
162        self.write(os, body)
163        self.write(os, footer[self.__closebody_index:])
164
165class View(Parametrized):
166    """Base class for Views. The base class provides a common interface, and
167    also handles common operations such as opening the file, and delegating
168    the view formatting to a strategy class."""
169
170    template = Parameter(Format(), 'the object that provides the html template for the view')
171
172    def __init__(self, **kwds):
173
174        super(View, self).__init__(**kwds)
175        self.main = False
176        self._id_counter = 0
177
178
179    def generate_id(self):
180        """Generate an id that is (at least) unique on a particular view,
181        and thus, html document."""
182
183        c = self._id_counter
184        self._id_counter += 1
185        return c
186
187    def register(self, frame):
188        """Registers this View class with its frame."""
189
190        self.frame = frame
191        self.directory_layout = self.frame.processor.directory_layout
192        self.processor = frame.processor
193        self.__os = None
194
195    def filename(self):
196        """Return the filename (currently) associated with the view."""
197
198        return ''
199
200    def title(self):
201        """Return the title (currently) associated with the view."""
202
203        return ''
204
205    def root(self):
206        """Return a pair of (url, label) to link to the entry point of this view."""
207
208        return None, None
209
210    def write_navigation_bar(self):
211        """Generate a navigation bar for this view."""
212
213        self.write(self.frame.navigation_bar(self) + '\n')
214
215    def os(self):
216        "Returns the output stream opened with start_file"
217
218        return self.__os
219
220    def write(self, str):
221        """Writes the given string to the currently opened file"""
222
223        self.__os.write(str)
224
225    def register_filenames(self):
226        """Register filenames for each file this View will generate."""
227
228        pass
229
230    def toc(self):
231        """Retrieves the TOC for this view. This method assumes that the view
232        generates info for the the whole ASG, which could be the Scope,
233        the Source (source code) or the XRef (cross reference info).
234        The default implementation returns None."""
235
236        pass
237
238    def process(self):
239        """Process the ASG, creating view-specific html pages."""
240
241        pass
242
243    def open_file(self):
244        """Returns a new output stream. This template method is for internal
245        use only, but may be overriden in derived classes.
246        The default joins output dir and self.filename()"""
247
248        path = os.path.join(self.processor.output, self.filename())
249        return open_file(path)
250
251    def close_file(self):
252        """Closes the internal output stream. This template method is for
253        internal use only, but may be overriden in derived classes."""
254
255        self.__os.close()
256        self.__os = None
257
258    def start_file(self, body='', headextra=''):
259        """Start a new file with given filename, title and body. This method
260        opens a file for writing, and writes the html header crap at the top.
261        You must specify a title, which is prepended with the project name.
262        The body argument is optional, and it is preferred to use stylesheets
263        for that sort of stuff. You may want to put an onLoad handler in it
264        though in which case that's the place to do it. The opened file is
265        stored and can be accessed using the os() method."""
266
267        self.__os = self.open_file()
268        prefix = rel(self.filename(), '')
269        self.template.init(self.processor, prefix)
270        if not body:
271            body = '<body class="%s" onload="load()">'%self.__class__.__name__
272        self.template.view_header(self.__os, self.title(), body, headextra, self)
273
274    def end_file(self, body='</body>'):
275        """Close the file using given close body tag. The default is
276        just a close body tag, but if you specify '' then nothing will be
277        written (useful for a frames view)"""
278
279        self.template.view_footer(self.__os, body)
280        self.close_file()
281
282    def reference(self, name, scope, label=None, **keys):
283        """Returns a reference to the given name. The name is a scoped name,
284        and the optional label is an alternative name to use as the link text.
285        The name is looked up in the TOC so the link may not be local. The
286        optional keys are appended as attributes to the A tag."""
287
288        if not label: label = escape(str(scope.prune(name)))
289        entry = self.processor.toc[name]
290        if entry: return href(rel(self.filename(), entry.link), label, **keys)
291        return label or ''
292