Modules | Files | Inheritance Tree | Inheritance Graph | Name Index | Config
File: Synopsis/Formatter/TexInfo.py
    1| #  $Id: TexInfo.py,v 1.5 2001/07/26 08:22:20 chalky Exp $
    2| #
    3| #  This file is a part of Synopsis.
    4| #  Copyright (C) 2000, 2001 Stefan Seefeld
    5| #
    6| #  Synopsis is free software; you can redistribute it and/or modify it
    7| #  under the terms of the GNU General Public License as published by
    8| #  the Free Software Foundation; either version 2 of the License, or
    9| #  (at your option) any later version.
   10| #
   11| #  This program is distributed in the hope that it will be useful,
   12| #  but WITHOUT ANY WARRANTY; without even the implied warranty of
   13| #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   14| #  General Public License for more details.
   15| #
   16| #  You should have received a copy of the GNU General Public License
   17| #  along with this program; if not, write to the Free Software
   18| #  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
   19| #  02111-1307, USA.
   20| #
   21| # $Log: TexInfo.py,v $
   22| # Revision 1.5  2001/07/26 08:22:20  chalky
   23| # Fixes 'bug' caused by bad template support
   24| #
   25| # Revision 1.4  2001/07/19 04:03:05  chalky
   26| # New .syn file format.
   27| #
   28| # Revision 1.3  2001/07/10 06:04:07  stefan
   29| # added support for info files to the TexInfo formatter
   30| #
   31| # Revision 1.2  2001/06/15 18:07:48  stefan
   32| # made TexInfo formatter useable. Removed all document structure stuff, such that the output can be embedded into any document level (as a chapter, a section, etc.)
   33| #
   34| # Revision 1.1  2001/06/11 19:45:15  stefan
   35| # initial work on a TexInfo formatter
   36| #
   37| #
   38| #
   39| """a TexInfo formatter """
   40| # THIS-IS-A-FORMATTER
   41| 
   42| import sys, getopt, os, os.path, string, re
   43| from Synopsis.Core import AST, Type, Util
   44| 
   45| verbose = 0
   46| 
   47| class Struct:
   48|     "Dummy class. Initialise with keyword args."
   49|     def __init__(self, **keys):
   50|         for name, value in keys.items(): setattr(self, name, value)
   51| 
   52| class CommentFormatter:
   53|     """A class that takes a comment Struct and formats its contents."""
   54| 
   55|     def parse(self, comm):
   56|         """Parse the comment struct"""
   57|         pass
   58| 
   59| class JavadocFormatter (CommentFormatter):
   60|     """A formatter that formats comments similar to Javadoc @tags"""
   61|     # @see IDL/Foo.Bar
   62|     # if a line starts with @tag then all further lines are tags
   63|     _re_see = '@see (([A-Za-z+]+)/)?(([A-Za-z_]+\.?)+)'
   64|     _re_tags = '(?P<text>.*?)\n[ \t]*(?P<tags>@[a-zA-Z]+[ \t]+.*)'
   65|     _re_see_line = '^[ \t]*@see[ \t]+(([A-Za-z+]+)/)?(([A-Za-z_]+\.?)+)(\([^)]*\))?([ \t]+(.*))?$'
   66|     _re_param = '^[ \t]*@param[ \t]+(?P<name>(A-Za-z+]+)([ \t]+(?P<desc>.*))?$'
   67| 
   68|     def __init__(self):
   69|         """Create regex objects for regexps"""
   70|         self.re_see = re.compile(self._re_see)
   71|         self.re_tags = re.compile(self._re_tags,re.M|re.S)
   72|         self.re_see_line = re.compile(self._re_see_line,re.M)
   73|     def parse(self, comm):
   74|         """Parse the comm.detail for @tags"""
   75|         comm.detail = self.parseText(comm.detail, comm.decl)
   76|         comm.summary = self.parseText(comm.summary, comm.decl)
   77|     def extract(self, regexp, str):
   78|         """Extracts all matches of the regexp from the text. The MatchObjects
   79|         are returned in a list"""
   80|         mo = regexp.search(str)
   81|         ret = []
   82|         while mo:
   83|             ret.append(mo)
   84|             start, end = mo.start(), mo.end()
   85|             str = str[:start] + str[end:]
   86|             mo = regexp.search(str, start)
   87|         return str, ret
   88| 
   89|     def parseTags(self, str, joiner):
   90|         """Returns text, tags"""
   91|         # Find tags
   92|         mo = self.re_tags.search(str)
   93|         if not mo: return str, ''
   94|         str, tags = mo.group('text'), mo.group('tags')
   95|         # Split the tag section into lines
   96|         tags = map(string.strip, string.split(tags,'\n'))
   97|         # Join non-tag lines to the previous tag
   98|         tags = reduce(joiner, tags, [])
   99|         return str, tags
  100| 
  101|     def parseText(self, str, decl):
  102|         if str is None: return str
  103|         #str, see = self.extract(self.re_see_line, str)
  104|         see_tags, attr_tags, param_tags, return_tag = [], [], [], None
  105|         joiner = lambda x,y: len(y) and y[0]=='@' and x+[y] or x[:-1]+[x[-1]+' '+y]
  106|         str, tags = self.parseTags(str, joiner)
  107|         # Parse each of the tags
  108|         for line in tags:
  109|             tag, rest = string.split(line,' ',1)
  110|             if tag == '@see':
  111|                see_tags.append(string.split(rest,' ',1))
  112|             elif tag == '@param':
  113|                param_tags.append(string.split(rest,' ',1))
  114|             elif tag == '@return':
  115|                return_tag = rest
  116|             elif tag == '@attr':
  117|                attr_tags.append(string.split(rest,' ',1))
  118|          else:
  119|                # Warning: unknown tag
  120|         pass
  121|         return "%s%s%s%s%s"%(
  122|             self.parse_see(str, decl),
  123|             self.format_params(param_tags),
  124|             self.format_attrs(attr_tags),
  125|             self.format_return(return_tag),
  126|             self.format_see(see_tags, decl)
  127|         )
  128|     def parse_see(self, str, decl):
  129|         """Parses inline @see tags"""
  130|         # Parse inline @see's  #TODO change to link or whatever javadoc uses
  131|         mo = self.re_see.search(str)
  132|         while mo:
  133|             groups, start, end = mo.groups(), mo.start(), mo.end()
  134|             lang = groups[1] or ''
  135|             #tag = self.find_link(groups[2], decl)
  136|             tag = groups[2]
  137|             str = str[:start] + tag + str[end:]
  138|             end = start + len(tag)
  139|             mo = self.re_see.search(str, end)
  140|         return str
  141|     def format_params(self, param_tags):
  142|         """Formats a list of (param, description) tags"""
  143|         if not len(param_tags): return ''
  144|         table = '@table @samp\n%s@end table\n'
  145|         return table%string.join(map(lambda p:'@item %s\n%s'%(p[0],p[1]), param_tags), '\n')
  146| 
  147|     def format_attrs(self, attr_tags):
  148|         """Formats a list of (attr, description) tags"""
  149|         if not len(attr_tags): return ''
  150|         table = '@table @samp\n%s@end table\n'
  151|         row = '@item %s\n%s\n'
  152|         return 'Attributes:\n' + table%string.join(map(lambda p,row=row: row%(p[0],p[1]), attr_tags))
  153|     def format_return(self, return_tag):
  154|         """Formats a since description string"""
  155|         if not return_tag: return ''
  156|         return "Return:\n" + return_tag
  157|     def format_see(self, see_tags, decl):
  158|         """Formats a list of (ref,description) tags"""
  159|         if not len(see_tags): return ''
  160|         #FIXME: add proper cross referencing
  161|         seestr = "See Also:"
  162|         seelist = []
  163|         for see in see_tags:
  164|             ref,desc = see[0], len(see)>1 and see[1] or ''
  165|             #tag = self.find_link(ref, decl)
  166|             tag = ref
  167|             seelist.append(tag+desc)
  168|         return seestr + string.join(seelist,'\n')
  169|     def find_link(self, ref, decl):
  170|         """Given a "reference" and a declaration, returns a HTML link.
  171|         Various methods are tried to resolve the reference. First the
  172|         parameters are taken off, then we try to split the ref using '.' or
  173|         '::'. The params are added back, and then we try to match this scoped
  174|         name against the current scope. If that fails, then we recursively try
  175|         enclosing scopes.
  176|         """
  177|         # Remove params
  178|         index, label = string.find(ref,'('), ref
  179|         if index >= 0:
  180|             params = ref[index:]
  181|             ref = ref[:index]
  182|         else:
  183|             params = ''
  184|         # Split ref
  185|         ref = string.split(ref, '.')
  186|         if len(ref) == 1:
  187|             ref = string.split(ref[0], '::')
  188|         # Add params back
  189|         ref = ref[:-1] + [ref[-1]+params]
  190|         # Find in all scopes
  191|         scope = list(decl.name())
  192|         while 1:
  193|             entry = self._find_link_at(ref, scope)
  194|             if entry: return href(entry.link, label)
  195|             if len(scope) == 0: break
  196|             del scope[-1]
  197|         # Not found
  198|         return label+" "
  199|     def _find_link_at(self, ref, scope):
  200|         # Try scope + ref[0]
  201|         entry = config.toc.lookup(scope+ref[:1])
  202|         if entry:
  203|             # Found.
  204|             if len(ref) > 1:
  205|                # Find sub-refs
  206|                entry = self._find_link_at(ref[1:], scope+ref[:1])
  207|                if entry:
  208|                    # Recursive sub-ref was okay!
  209|                  return entry 
  210|          else:
  211|                # This was the last scope in ref. Done!
  212|                return entry
  213|         # Try a method name match:
  214|         if len(ref) == 1:
  215|             entry = self._find_method_entry(ref[0], scope)
  216|             if entry: return entry
  217|         # Not found at this scope
  218|         return None
  219|     def _find_method_entry(self, name, scope):
  220|         """Tries to find a TOC entry for a method adjacent to decl. The
  221|         enclosing scope is found using the types dictionary, and the
  222|         realname()'s of all the functions compared to ref."""
  223|         try:
  224|             scope = config.types[scope]
  225|         except KeyError:
  226|             #print "No parent scope:",decl.name()[:-1]
  227|             return None
  228|         if not scope: return None
  229|         if not isinstance(scope, Type.Declared): return None
  230|         scope = scope.declaration()
  231|         if not isinstance(scope, AST.Scope): return None
  232|         for decl in scope.declarations():
  233|             if isinstance(decl, AST.Function):
  234|                if decl.realname()[-1] == name:
  235|                    return config.toc.lookup(decl.name())
  236|         # Failed
  237|         return None
  238| 
  239| def _replace(comm, old, new):
  240|     comm.summary = string.replace(comm.summary, old, new)
  241|     comm.detail = string.replace(comm.detail, old, new)
  242| 
  243| class Escapifier(CommentFormatter):
  244|     """escapify the strings to become valid texinfo text.
  245|     Only replace '@' by '@@' if these are not part of valid texinfo tags."""
  246| 
  247|     tags = ['table', 'item', 'samp', 'end'] #etc.
  248|     special = ['{', '}',]
  249|     def __init__(self):
  250|         self.__re_texi = re.compile('@(?!(' + string.join(Escapifier.tags, '|') + '))')
  251|     def parse(self, comm):
  252|         comm.summary = self.__re_texi.sub('@@', comm.summary)
  253|         comm.detail = self.__re_texi.sub('@@', comm.detail)
  254|         comm.summary = reduce(lambda x,y: string.replace(x, y , '@' + y), Escapifier.special, comm.summary)
  255|         comm.detail = reduce(lambda x,y: string.replace(x, y, '@' + y), Escapifier.special, comm.detail)
  256| 
  257| commentFormatters = {
  258|     'none' : CommentFormatter,
  259|     'javadoc' : JavadocFormatter,
  260|     'escapify' : Escapifier,
  261| }
  262| 
  263| class MenuMaker (AST.Visitor):
  264|     """generate a texinfo menu for the declarations of a given scope"""
  265|     def __init__(self, scope, os):
  266|         self.__scope = scope
  267|         self.__os = os
  268|     def write(self, text): self.__os.write(text)
  269|     def start(self): self.write('@menu\n')
  270|     def end(self): self.write('@end menu\n')
  271|     def visitDeclaration(self, node):
  272|         name = reduce(lambda x,y: string.replace(x, y , '@' + y), Escapifier.special, Util.dotName(node.name(), self.__scope))
  273|         self.write('* ' + name + '::\t' + node.type() + '\n')
  274|     visitGroup = visitDeclaration
  275|     visitEnum = visitDeclaration
  276| 
  277| class Formatter (Type.Visitor, AST.Visitor):
  278|     """
  279|     The type visitors should generate names relative to the current scope.
  280|     The generated references however are fully scoped names
  281|     """
  282|     def __init__(self, os):
  283|         self.__os = os
  284|         self.__scope = []
  285|         self.__indent = 0
  286|         self.__comment_formatters = [JavadocFormatter(), Escapifier()]
  287|     def scope(self): return self.__scope
  288|     def write(self, text): self.__os.write(text)
  289| 
  290|     def escapify(self, label):
  291|         return reduce(lambda x,y: string.replace(x, y , '@' + y), Escapifier.special, label)
  292| 
  293|     #def reference(self, ref, label):
  294|     #    """reference takes two strings, a reference (used to look up the symbol and generated the reference),
  295|     #    and the label (used to actually write it)"""
  296|     #    location = self.__toc.lookup(ref)
  297|     #    if location != "": return href("#" + location, label)
  298|     #    else: return span("type", str(label))
  299|         
  300|     #def label(self, ref):
  301|     #    location = self.__toc.lookup(Util.ccolonName(ref))
  302|     #    ref = Util.ccolonName(ref, self.scope())
  303|     #    if location != "": return name("\"" + location + "\"", ref)
  304|     #    else: return ref
  305| 
  306|     def type_label(self): return self.escapify(self.__type_label)
  307| 
  308|     def decl_label(self, decl): return self.escapify(decl[-1])
  309| 
  310|     def formatType(self, type):
  311|         "Returns a reference string for the given type object"
  312|         if type is None: return "(unknown)"
  313|         type.accept(self)
  314|         return self.type_label()
  315| 
  316|     def formatComments(self, decl):
  317|         strlist = map(lambda x:str(x), decl.comments())
  318|         #doc = map(lambda c, this=self: this.__comment_formatter.parse(c), strlist)
  319|         comm = Struct(detail=string.join(strlist), summary='', has_detail=1, decl=decl)
  320|         if comm.detail: map(lambda f,c=comm: f.parse(c), self.__comment_formatters)
  321|         else: comm.has_detail=0
  322|         self.write(comm.detail + '\n')
  323| 
  324|     #################### Type Visitor ###########################################
  325| 
  326|     def visitBaseType(self, type):
  327|         self.__type_ref = Util.ccolonName(type.name())
  328|         self.__type_label = Util.ccolonName(type.name())
  329|         
  330|     def visitUnknown(self, type):
  331|         self.__type_ref = Util.ccolonName(type.name())
  332|         self.__type_label = Util.ccolonName(type.name(), self.scope())
  333|         
  334|     def visitDeclared(self, type):
  335|         self.__type_label = Util.ccolonName(type.name(), self.scope())
  336|         self.__type_ref = Util.ccolonName(type.name())
  337|         
  338|     def visitModifier(self, type):
  339|         type.alias().accept(self)
  340|         self.__type_ref = string.join(type.premod()) + " " + self.__type_ref + " " + string.join(type.postmod())
  341|         self.__type_label = string.join(type.premod()) + " " + self.__type_label + " " + string.join(type.postmod())
  342|             
  343|     def visitParametrized(self, type):
  344|         if type.template():
  345|             type.template().accept(self)
  346|             type_label = self.__type_label + "<"
  347|         else: type_label = "(unknown)<"
  348|         parameters_label = []
  349|         for p in type.parameters():
  350|             p.accept(self)
  351|             parameters_label.append(self.__type_label)
  352|         self.__type_label = type_label + string.join(parameters_label, ", ") + ">"
  353| 
  354|     def visitFunctionType(self, type):
  355|         # TODO: this needs to be implemented
  356|         self.__type_ref = 'function_type'
  357|         self.__type_label = 'function_type'
  358| 
  359| 
  360|     #################### AST Visitor ############################################
  361|             
  362|     def visitDeclarator(self, node):
  363|         self.__declarator = node.name()
  364|         for i in node.sizes():
  365|             self.__declarator[-1] = self.__declarator[-1] + "[" + str(i) + "]"
  366| 
  367|     def visitTypedef(self, typedef):
  368|         #self.write('@node ' + self.decl_label(typedef.name()) + '\n')
  369|         self.write('@deftp ' + typedef.type() + ' {' + self.formatType(typedef.alias()) + '} {' + self.decl_label(typedef.name()) + '}\n')
  370|         self.formatComments(typedef)
  371|         self.write('@end deftp\n')
  372|     def visitVariable(self, variable):
  373|         #self.write('@node ' + self.decl_label(variable.name()) + '\n')
  374|         self.write('@deftypevr {' + variable.type() + '} {' + self.formatType(variable.vtype()) + '} {' + self.decl_label(variable.name()) + '}\n')
  375|         #FIXME !: how can values be represented in texinfo ?
  376|         self.formatComments(variable)
  377|         self.write('@end deftypevr\n')
  378| 
  379|     def visitConst(self, const):
  380|         print "sorry, <const> not implemented"
  381| 
  382|     def visitModule(self, module):
  383|         #self.write('@node ' + self.decl_label(module.name()) + '\n')
  384|         self.write('@deftp ' + module.type() + ' ' + self.decl_label(module.name()) + '\n')
  385|         self.formatComments(module)
  386|         self.scope().append(module.name()[-1])
  387|         #menu = MenuMaker(self.scope(), self.__os)
  388|         #menu.start()
  389|         #for declaration in module.declarations(): declaration.accept(menu)
  390|         #menu.end()
  391|         for declaration in module.declarations(): declaration.accept(self)
  392|         self.scope().pop()
  393|         self.write('@end deftp\n')
  394| 
  395|     def visitClass(self, clas):
  396|         #self.write('@node ' + self.decl_label(clas.name()) + '\n')
  397|         self.write('@deftp ' + clas.type() + ' ' + self.decl_label(clas.name()) + '\n')
  398|         if len(clas.parents()):
  399|             self.write('parents:')
  400|             first = 1
  401|             for parent in clas.parents():
  402|                 if not first: self.write(', ')
  403|                 else: self.write(' ')
  404|                 parent.accept(self)
  405|             self.write('\n')
  406|         self.formatComments(clas)
  407|         self.scope().append(clas.name()[-1])
  408|         #menu = MenuMaker(self.scope(), self.__os)
  409|         #menu.start()
  410|         #for declaration in clas.declarations(): declaration.accept(menu)
  411|         #menu.end()
  412|         for declaration in clas.declarations(): declaration.accept(self)
  413|         self.scope().pop()
  414|         self.write('@end deftp\n')
  415| 
  416|     def visitInheritance(self, inheritance):
  417|         #map(lambda a, this=self: this.entity("modifier", a), inheritance.attributes())
  418|         #self.entity("classname", Util.ccolonName(inheritance.parent().name(), self.scope()))
  419|         self.write('parent class')
  420| 
  421|     def visitParameter(self, parameter):
  422|         #map(lambda m, this=self: this.entity("modifier", m), parameter.premodifier())
  423|         parameter.type().accept(self)
  424|         label = self.write('{' + self.type_label() + '}')
  425|         label = self.write(' @var{' + parameter.identifier() + '}')
  426|         #map(lambda m, this=self: this.entity("modifier", m), parameter.postmodifier())
  427| 
  428|     def visitFunction(self, function):
  429|         ret = function.returnType()
  430|         if ret:
  431|             ret.accept(self)
  432|             ret_label = '{' + self.type_label() + '}'
  433|         else:
  434|             ret_label = '{}'
  435|         #self.write('@node ' + self.decl_label(function.realname()) + '\n')
  436|         self.write('@deftypefn ' + function.type() + ' ' + ret_label + ' ' + self.decl_label(function.realname()) + '(')
  437|         first = 1
  438|         for parameter in function.parameters():
  439|             if not first: self.write(', ')
  440|             else: self.write(' ')
  441|             parameter.accept(self)
  442|             first = 0
  443|         self.write(')\n')
  444|         #    map(lambda e, this=self: this.entity("exceptionname", e), operation.exceptions())
  445|         self.formatComments(function)
  446|         self.write('@end deftypefn\n')
  447| 
  448|     def visitOperation(self, operation):
  449|         ret = operation.returnType()
  450|         if ret:
  451|             ret.accept(self)
  452|             ret_label = '{' + self.type_label() + '}'
  453|         else:
  454|             ret_label = '{}'
  455|         try:
  456|             #self.write('@node ' + self.decl_label(operation.name()) + '\n')
  457|             self.write('@deftypeop ' + operation.type() + ' ' + self.decl_label(self.scope()) + ' ' + ret_label + ' ' + self.decl_label(operation.realname()) + '(')
  458|         except:
  459|             print operation.realname()
  460|             sys.exit(0)
  461|         first = 1
  462|         for parameter in operation.parameters():
  463|             if not first: self.write(', ')
  464|             else: self.write(' ')
  465|             parameter.accept(self)
  466|             first = 0
  467|         self.write(')\n')
  468|         #    map(lambda e, this=self: this.entity("exceptionname", e), operation.exceptions())
  469|         self.formatComments(operation)
  470|         self.write('@end deftypeop\n')
  471| 
  472|     def visitEnumerator(self, enumerator):
  473|         self.write('@deftypevr {' + enumerator.type() + '} {} {' + self.decl_label(enumerator.name()) + '}')
  474|         #FIXME !: how can values be represented in texinfo ?
  475|         if enumerator.value(): self.write('\n')
  476|         else: self.write('\n')
  477|         self.formatComments(enumerator)
  478|         self.write('@end deftypevr\n')
  479|     def visitEnum(self, enum):
  480|         #self.write('@node ' + self.decl_label(enum.name()) + '\n')
  481|         self.write('@deftp ' + enum.type() + ' ' + self.decl_label(enum.name()) + '\n')
  482|         self.formatComments(enum)
  483|         for enumerator in enum.enumerators(): enumerator.accept(self)
  484|         self.write('@end deftp\n')
  485| 
  486| def usage():
  487|     """Print usage to stdout"""
  488|     print \
  489| """
  490|   -o <filename>                        Output filename"""
  491| 
  492| def __parseArgs(args):
  493|     global output, verbose
  494|     output = sys.stdout
  495|     try:
  496|         opts,remainder = getopt.getopt(args, "o:v")
  497|     except getopt.error, e:
  498|         sys.stderr.write("Error in arguments: " + str(e) + "\n")
  499|         sys.exit(1)
  500| 
  501|     for opt in opts:
  502|         o,a = opt
  503|         if o == "-o": output = open(a, "w")
  504|         elif o == "-v": verbose = 1
  505| 
  506| def format(args, ast, config):
  507|     global output
  508|     __parseArgs(args)
  509|     #menu = MenuMaker([], output)
  510|     #menu.start()
  511|     #for d in declarations:
  512|     #    d.accept(menu)
  513|     #menu.end()
  514|     formatter = Formatter(output)
  515|     for d in ast.declarations():
  516|         d.accept(formatter)