Modules | Files | Inheritance Tree | Inheritance Graph | Name Index | Config
File: Synopsis/Formatter/HTML/CommentFormatter.py
    1| # $Id: CommentFormatter.py,v 1.20 2003/01/20 06:43:02 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: CommentFormatter.py,v $
   23| # Revision 1.20  2003/01/20 06:43:02  chalky
   24| # Refactored comment processing. Added AST.CommentTag. Linker now determines
   25| # comment summary and extracts tags. Increased AST version number.
   26| #
   27| # Revision 1.19  2003/01/16 18:54:03  chalky
   28| # Simplified re_tags regexp
   29| #
   30| # Revision 1.18  2002/11/01 07:18:15  chalky
   31| # Added the QuoteHTML formatter
   32| #
   33| # Revision 1.17  2002/04/26 01:21:14  chalky
   34| # Bugs and cleanups
   35| #
   36| # Revision 1.16  2001/06/28 07:22:18  stefan
   37| # more refactoring/cleanup in the HTML formatter
   38| #
   39| # Revision 1.15  2001/04/17 13:35:37  chalky
   40| # Slightly more robust
   41| #
   42| # Revision 1.14  2001/04/05 14:03:21  chalky
   43| # Very small change
   44| #
   45| # Revision 1.13  2001/04/03 23:18:47  chalky
   46| # Fixed has_detail for decls with no comments
   47| #
   48| # Revision 1.12  2001/04/03 11:36:24  chalky
   49| # Made comments show detail by default
   50| #
   51| # Revision 1.11  2001/03/29 14:09:55  chalky
   52| # newlines were getting eaten in tags. made attr list a table
   53| #
   54| # Revision 1.10  2001/03/28 13:11:04  chalky
   55| # Added @attr tag to Javadoc formatter - very similar to @param tags :)
   56| #
   57| # Revision 1.9  2001/03/28 12:55:19  chalky
   58| # Sanity checks
   59| #
   60| # Revision 1.8  2001/02/13 05:19:31  chalky
   61| # @see links are done a bit more methodically
   62| #
   63| # Revision 1.7  2001/02/12 04:08:09  chalky
   64| # Added config options to HTML and Linker. Config demo has doxy and synopsis styles.
   65| #
   66| # Revision 1.6  2001/02/07 17:00:43  chalky
   67| # Added Qt-style comments support
   68| #
   69| # Revision 1.5  2001/02/07 16:14:40  chalky
   70| # Fixed @see linking to be more stubborn (still room for improv. tho)
   71| #
   72| # Revision 1.4  2001/02/07 15:31:06  chalky
   73| # Rewrite javadoc formatter. Now all tags must be at the end of the comment,
   74| # except for inline tags
   75| #
   76| # Revision 1.3  2001/02/01 16:47:48  chalky
   77| # Added CommentFormatter as base of the Comment Formatters ...
   78| #
   79| # Revision 1.2  2001/02/01 15:23:24  chalky
   80| # Copywritten brown paper bag edition.
   81| #
   82| #
   83| 
   84| """CommentParser, CommentFormatter and derivatives."""
   85| 
   86| # System modules
   87| import re, string
   88| 
   89| # Synopsis modules
   90| from Synopsis.Core import AST, Type, Util
   91| 
   92| # HTML modules
   93| import core
   94| from core import config
   95| from Tags import *
   96| 
   97| class CommentFormatter:
   98|     """A class that takes a Declaration and formats its comments into a string."""
   99|     def __init__(self):
  100|         self.__formatters = core.config.commentFormatterList
  101|         # Cache the bound methods
  102|         self.__format_methods = map(lambda f:f.format, self.__formatters)
  103|         self.__format_summary_methods = map(lambda f:f.format_summary, self.__formatters)
  104|         # Weed out the unneccessary calls to the empty base methods
  105|         base = CommentFormatterStrategy.format.im_func
  106|         self.__format_methods = filter(
  107|             lambda m, base=base: m.im_func is not base, self.__format_methods)
  108|         base = CommentFormatterStrategy.format_summary.im_func
  109|         self.__format_summary_methods = filter(
  110|             lambda m, base=base: m.im_func is not base, self.__format_summary_methods)
  111| 
  112|     def format(self, page, decl):
  113|         """Formats the first comment of the given AST.Declaration.
  114|         Note that the Linker.Comments.Summarizer CommentProcessor is supposed
  115|         to have combined all comments first in the Linker stage.
  116|         @return the formatted text
  117|         """
  118|         comments = decl.comments()
  119|         if len(comments) == 0: return ''
  120|         text = comments[0].text()
  121|         if not text: return ''
  122|         # Let each strategy format the text in turn
  123|         for method in self.__format_methods:
  124|             text = method(page, decl, text)
  125|         return text
  126| 
  127|     def format_summary(self, page, decl):
  128|         """Formats the summary of the first comment of the given
  129|         AST.Declaration.
  130|         Note that the Linker.Comments.Summarizer CommentProcessor is supposed
  131|         to have combined all comments first in the Linker stage.
  132|         @return the formatted summary text
  133|         """
  134|         comments = decl.comments()
  135|         if len(comments) == 0: return ''
  136|         text = comments[0].summary()
  137|         if not text: return ''
  138|         # Let each strategy format the text in turn
  139|         for method in self.__format_summary_methods:
  140|             text = method(page, decl, text)
  141|         return text
  142| 
  143| class CommentFormatterStrategy:
  144|     """Interface class that takes a comment and formats its summary and/or
  145|     detail strings."""
  146| 
  147|     def format(self, page, decl, text):
  148|         """Format the given comment
  149|         @param page the Page to use for references and determining the correct
  150|         relative filename.
  151|         @param decl the declaration
  152|         @param text the comment text to format
  153|         """
  154|         pass
  155|     def format_summary(self, page, decl, summary):
  156|         """Format the given comment summary
  157|         @param page the Page to use for references and determining the correct
  158|         relative filename.
  159|         @param decl the declaration
  160|         @param summary the comment summary to format
  161|         """
  162|         pass
  163| 
  164| class QuoteHTML (CommentFormatterStrategy):
  165|     """A formatter that quotes HTML characters like the angle brackets and the
  166|     ampersand. Formats both text and summary."""
  167|     def format(self, page, decl, text):
  168|         """Replace angle brackets with HTML codes"""
  169|         text = text.replace('&', '&')
  170|         text = text.replace('<', '&lt;')
  171|         text = text.replace('>', '&gt;')
  172|         return text
  173|     def format_summary(self, page, decl, text):
  174|         """Replace angle brackets with HTML codes"""
  175|         text = text.replace('&', '&amp;')
  176|         text = text.replace('<', '&lt;')
  177|         text = text.replace('>', '&gt;')
  178|         return text
  179| 
  180| class JavadocFormatter (CommentFormatterStrategy):
  181|     """A formatter that formats comments similar to Javadoc @tags"""
  182|     # @see IDL/Foo.Bar
  183|     _re_see = '@see (([A-Za-z+]+)/)?(([A-Za-z_]+\.?)+)'
  184|     _re_see_line = '^[ \t]*@see[ \t]+(([A-Za-z+]+)/)?(([A-Za-z_]+\.?)+)(\([^)]*\))?([ \t]+(.*))?$'
  185|     _re_param = '^[ \t]*@param[ \t]+(?P<name>(A-Za-z+]+)([ \t]+(?P<desc>.*))?$'
  186| 
  187|     def __init__(self):
  188|         """Create regex objects for regexps"""
  189|         self.re_see = re.compile(self._re_see)
  190|         self.re_see_line = re.compile(self._re_see_line,re.M)
  191|     def extract(self, regexp, str):
  192|         """Extracts all matches of the regexp from the text. The MatchObjects
  193|         are returned in a list"""
  194|         mo = regexp.search(str)
  195|         ret = []
  196|         while mo:
  197|             ret.append(mo)
  198|             start, end = mo.start(), mo.end()
  199|             str = str[:start] + str[end:]
  200|             mo = regexp.search(str, start)
  201|         return str, ret
  202| 
  203|     def format(self, page, decl, text):
  204|         """Format any @tags in the text, and any @tags stored by the JavaTags
  205|         CommentProcessor in the Linker stage."""
  206|         if text is None: return text
  207|         see_tags, attr_tags, param_tags, return_tag = [], [], [], None
  208|         tags = decl.comments()[0].tags()
  209|         # Parse each of the tags
  210|         for tag in tags:
  211|             name, rest = tag.name(), tag.text()
  212|             if name == '@see':
  213|                see_tags.append(string.split(rest,' ',1))
  214|             elif name == '@param':
  215|                param_tags.append(string.split(rest,' ',1))
  216|             elif name == '@return':
  217|                return_tag = rest
  218|             elif name == '@attr':
  219|                attr_tags.append(string.split(rest,' ',1))
  220|          else:
  221|                # unknown tag
  222|         pass
  223|         return "%s%s%s%s%s"%(
  224|             self.format_inline_see(page, decl, text),
  225|             self.format_params(param_tags),
  226|             self.format_attrs(attr_tags),
  227|             self.format_return(return_tag),
  228|             self.format_see(page, see_tags, decl)
  229|         )
  230|     def format_inline_see(self, page, decl, text):
  231|         """Formats inline @see tags in the text"""
  232|         #TODO change to link or whatever javadoc uses
  233|         mo = self.re_see.search(text)
  234|         while mo:
  235|             groups, start, end = mo.groups(), mo.start(), mo.end()
  236|             lang = groups[1] or ''
  237|             link = self.find_link(page, groups[2], decl)
  238|             text = text[:start] + link + text[end:]
  239|             end = start + len(link)
  240|             mo = self.re_see.search(text, end)
  241|         return text
  242|     def format_params(self, param_tags):
  243|         """Formats a list of (param, description) tags"""
  244|         if not len(param_tags): return ''
  245|         return div('tag-heading',"Parameters:") + \
  246|                div('tag-section', string.join(
  247|                    map(lambda p:"<b>%s</b> - %s"%(p[0],p[1]), param_tags),
  248|                '<br>'
  249|         )
  250|         )
  251|     def format_attrs(self, attr_tags):
  252|         """Formats a list of (attr, description) tags"""
  253|         if not len(attr_tags): return ''
  254|         table = '<table border=1 class="attr-table">%s</table>'
  255|         row = '<tr><td valign="top" class="attr-table-name">%s</td><td class="attr-table-desc">%s</td></tr>'
  256|         return div('tag-heading',"Attributes:") + \
  257|                table%string.join(
  258|                    map(lambda p,row=row:row%(p[0],p[1]), attr_tags)
  259|         )
  260|     def format_return(self, return_tag):
  261|         """Formats a since description string"""
  262|         if not return_tag: return ''
  263|         return div('tag-heading',"Return:")+div('tag-section',return_tag)
  264|     def format_see(self, page, see_tags, decl):
  265|         """Formats a list of (ref,description) tags"""
  266|         if not len(see_tags): return ''
  267|         seestr = div('tag-heading', "See Also:")
  268|         seelist = []
  269|         for see in see_tags:
  270|             ref,desc = see[0], len(see)>1 and see[1] or ''
  271|             link = self.find_link(page, ref, decl)
  272|             seelist.append(link + desc)
  273|         return seestr + div('tag-section', string.join(seelist,'\n<br>\n'))
  274|     def find_link(self, page, ref, decl):
  275|         """Given a "reference" and a declaration, returns a HTML link.
  276|         Various methods are tried to resolve the reference. First the
  277|         parameters are taken off, then we try to split the ref using '.' or
  278|         '::'. The params are added back, and then we try to match this scoped
  279|         name against the current scope. If that fails, then we recursively try
  280|         enclosing scopes.
  281|         """
  282|         # Remove params
  283|         index, label = string.find(ref,'('), ref
  284|         if index >= 0:
  285|             params = ref[index:]
  286|             ref = ref[:index]
  287|         else:
  288|             params = ''
  289|         # Split ref
  290|         ref = string.split(ref, '.')
  291|         if len(ref) == 1:
  292|             ref = string.split(ref[0], '::')
  293|         # Add params back
  294|         ref = ref[:-1] + [ref[-1]+params]
  295|         # Find in all scopes
  296|         scope = list(decl.name())
  297|         while 1:
  298|             entry = self._find_link_at(ref, scope)
  299|             if entry:
  300|                url = rel(page.filename(), entry.link)
  301|                return href(url, label)
  302|             if len(scope) == 0: break
  303|             del scope[-1]
  304|         # Not found
  305|         return label+" "
  306|     def _find_link_at(self, ref, scope):
  307|         # Try scope + ref[0]
  308|         entry = config.toc.lookup(scope+ref[:1])
  309|         if entry:
  310|             # Found.
  311|             if len(ref) > 1:
  312|                # Find sub-refs
  313|                entry = self._find_link_at(ref[1:], scope+ref[:1])
  314|                if entry:
  315|                    # Recursive sub-ref was okay!
  316|                  return entry 
  317|          else:
  318|                # This was the last scope in ref. Done!
  319|                return entry
  320|         # Try a method name match:
  321|         if len(ref) == 1:
  322|             entry = self._find_method_entry(ref[0], scope)
  323|             if entry: return entry
  324|         # Not found at this scope
  325|         return None
  326|     def _find_method_entry(self, name, scope):
  327|         """Tries to find a TOC entry for a method adjacent to decl. The
  328|         enclosing scope is found using the types dictionary, and the
  329|         realname()'s of all the functions compared to ref."""
  330|         try:
  331|             scope = config.types[scope]
  332|         except KeyError:
  333|             #print "No parent scope:",decl.name()[:-1]
  334|             return None
  335|         if not scope: return None
  336|         if not isinstance(scope, Type.Declared): return None
  337|         scope = scope.declaration()
  338|         if not isinstance(scope, AST.Scope): return None
  339|         for decl in scope.declarations():
  340|             if isinstance(decl, AST.Function):
  341|                if decl.realname()[-1] == name:
  342|                    return config.toc.lookup(decl.name())
  343|         # Failed
  344|         return None
  345| 
  346| class QtDocFormatter (JavadocFormatter):
  347|     """A formatter that uses Qt-style doc tags."""
  348|     _re_see = '@see (([A-Za-z+]+)/)?(([A-Za-z_]+\.?)+)'
  349|     _re_tags = r'((?P<text>.*?)\n)?[ \t]*(?P<tags>\\[a-zA-Z]+[ \t]+.*)'
  350|     _re_seealso = '[ \t]*(,|and|,[ \t]*and)[ \t]*'
  351|     def __init__(self):
  352|         JavadocFormatter.__init__(self)
  353|         self.re_seealso = re.compile(self._re_seealso)
  354| 
  355|     def parseText(self, str, decl):
  356|         if str is None: return str
  357|         #str, see = self.extract(self.re_see_line, str)
  358|         see_tags, param_tags, return_tag = [], [], None
  359|         joiner = lambda x,y: len(y) and y[0]=='\\' and x+[y] or x[:-1]+[x[-1]+y]
  360|         str, tags = self.parseTags(str, joiner)
  361|         # Parse each of the tags
  362|         for line in tags:
  363|             tag, rest = string.split(line,' ',1)
  364|             if tag == '\\sa':
  365|                see_tags.extend(
  366|                    map(lambda x: [x,''], self.re_seealso.split(rest))
  367|         )
  368|             elif tag == '\\param':
  369|                param_tags.append(string.split(rest,' ',1))
  370|             elif tag == '\\return':
  371|                return_tag = rest
  372|          else:
  373|                # Warning: unknown tag
  374|         pass
  375|         return "%s%s%s%s"%(
  376|             self.parse_see(str, decl),
  377|             self.format_params(param_tags),
  378|             self.format_return(return_tag),
  379|             self.format_see(see_tags, decl)
  380|         )
  381|     def format_see(self, see_tags, decl):
  382|         """Formats a list of (ref,description) tags"""
  383|         if not len(see_tags): return ''
  384|         seestr = div('tag-see-header', "See Also:")
  385|         seelist = []
  386|         for see in see_tags:
  387|             ref,desc = see[0], len(see)>1 and see[1] or ''
  388|             tag = self.re_seealso.match(ref) and ' %s '%ref or self.find_link(ref, decl)
  389|             seelist.append(span('tag-see', tag+desc))
  390|         return seestr + string.join(seelist,'')
  391| 
  392| class SectionFormatter (CommentFormatterStrategy):
  393|     """A test formatter"""
  394|     __re_break = '\n[ \t]*\n'
  395| 
  396|     def __init__(self):
  397|         self.re_break = re.compile(SectionFormatter.__re_break)
  398|     def format(self, page, decl, text):
  399|         if text is None: return text
  400|         para = '</p>\n<p>'
  401|         mo = self.re_break.search(text)
  402|         while mo:
  403|             start, end = mo.start(), mo.end()
  404|             text = text[:start] + para + text[end:]
  405|             end = start + len(para)
  406|             mo = self.re_break.search(text, end)
  407|         return '<p>%s</p>'%text
  408| 
  409| 
  410| 
  411| commentFormatters = {
  412|     'none' : CommentFormatterStrategy,
  413|     'ssd' : CommentFormatterStrategy,
  414|     'java' : CommentFormatterStrategy,
  415|     'quotehtml' : QuoteHTML,
  416|     'summary' : CommentFormatterStrategy,
  417|     'javadoc' : JavadocFormatter,
  418|     'qtdoc' : QtDocFormatter,
  419|     'section' : SectionFormatter,
  420| }
  421| 
  422|