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('<', '<')
171| text = text.replace('>', '>')
172| return text
173| def format_summary(self, page, decl, text):
174| """Replace angle brackets with HTML codes"""
175| text = text.replace('&', '&')
176| text = text.replace('<', '<')
177| text = text.replace('>', '>')
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|