| # Content (c) 2002, 2004, 2006, 2007, 2008 David Turner <david@freetype.org> |
| # |
| # This file contains routines used to parse the content of documentation |
| # comment blocks and build more structured objects out of them. |
| # |
| |
| from sources import * |
| from utils import * |
| import string, re |
| |
| |
| # this regular expression is used to detect code sequences. these |
| # are simply code fragments embedded in '{' and '}' like in: |
| # |
| # { |
| # x = y + z; |
| # if ( zookoo == 2 ) |
| # { |
| # foobar(); |
| # } |
| # } |
| # |
| # note that indentation of the starting and ending accolades must be |
| # exactly the same. the code sequence can contain accolades at greater |
| # indentation |
| # |
| re_code_start = re.compile( r"(\s*){\s*$" ) |
| re_code_end = re.compile( r"(\s*)}\s*$" ) |
| |
| |
| # this regular expression is used to isolate identifiers from |
| # other text |
| # |
| re_identifier = re.compile( r'(\w*)' ) |
| |
| |
| |
| ############################################################################# |
| # |
| # The DocCode class is used to store source code lines. |
| # |
| # 'self.lines' contains a set of source code lines that will be dumped as |
| # HTML in a <PRE> tag. |
| # |
| # The object is filled line by line by the parser; it strips the leading |
| # "margin" space from each input line before storing it in 'self.lines'. |
| # |
| class DocCode: |
| |
| def __init__( self, margin, lines ): |
| self.lines = [] |
| self.words = None |
| |
| # remove margin spaces |
| for l in lines: |
| if string.strip( l[:margin] ) == "": |
| l = l[margin:] |
| self.lines.append( l ) |
| |
| def dump( self, prefix = "", width=60 ): |
| lines = self.dump_lines( 0, width ) |
| for l in lines: |
| print prefix + l |
| |
| def dump_lines( self, margin=0, width=60 ): |
| result = [] |
| for l in self.lines: |
| result.append( " "*margin + l ) |
| return result |
| |
| |
| |
| ############################################################################# |
| # |
| # The DocPara class is used to store "normal" text paragraph. |
| # |
| # 'self.words' contains the list of words that make up the paragraph |
| # |
| class DocPara: |
| |
| def __init__( self, lines ): |
| self.lines = None |
| self.words = [] |
| for l in lines: |
| l = string.strip( l ) |
| self.words.extend( string.split( l ) ) |
| |
| def dump( self, prefix = "", width = 60 ): |
| lines = self.dump_lines( 0, width ) |
| for l in lines: |
| print prefix + l |
| |
| def dump_lines( self, margin=0, width = 60 ): |
| cur = "" # current line |
| col = 0 # current width |
| result = [] |
| |
| for word in self.words: |
| ln = len( word ) |
| if col > 0: |
| ln = ln+1 |
| |
| if col + ln > width: |
| result.append( " "*margin + cur ) |
| cur = word |
| col = len( word ) |
| else: |
| if col > 0: |
| cur = cur + " " |
| cur = cur + word |
| col = col + ln |
| |
| if col > 0: |
| result.append( " "*margin + cur ) |
| |
| return result |
| |
| |
| |
| ############################################################################# |
| # |
| # The DocField class is used to store a list containing either DocPara or |
| # DocCode objects. Each DocField also has an optional "name" which is used |
| # when the object corresponds to a field or value definition |
| # |
| class DocField: |
| |
| def __init__( self, name, lines ): |
| self.name = name # can be None for normal paragraphs/sources |
| self.items = [] # list of items |
| |
| mode_none = 0 # start parsing mode |
| mode_code = 1 # parsing code sequences |
| mode_para = 3 # parsing normal paragraph |
| |
| margin = -1 # current code sequence indentation |
| cur_lines = [] |
| |
| # now analyze the markup lines to see if they contain paragraphs, |
| # code sequences or fields definitions |
| # |
| start = 0 |
| mode = mode_none |
| for l in lines: |
| |
| # are we parsing a code sequence ? |
| if mode == mode_code: |
| |
| m = re_code_end.match( l ) |
| if m and len( m.group( 1 ) ) <= margin: |
| # that's it, we finised the code sequence |
| code = DocCode( 0, cur_lines ) |
| self.items.append( code ) |
| margin = -1 |
| cur_lines = [] |
| mode = mode_none |
| else: |
| # nope, continue the code sequence |
| cur_lines.append( l[margin:] ) |
| else: |
| # start of code sequence ? |
| m = re_code_start.match( l ) |
| if m: |
| # save current lines |
| if cur_lines: |
| para = DocPara( cur_lines ) |
| self.items.append( para ) |
| cur_lines = [] |
| |
| # switch to code extraction mode |
| margin = len( m.group( 1 ) ) |
| mode = mode_code |
| |
| else: |
| if not string.split( l ) and cur_lines: |
| # if the line is empty, we end the current paragraph, |
| # if any |
| para = DocPara( cur_lines ) |
| self.items.append( para ) |
| cur_lines = [] |
| else: |
| # otherwise, simply add the line to the current |
| # paragraph |
| cur_lines.append( l ) |
| |
| if mode == mode_code: |
| # unexpected end of code sequence |
| code = DocCode( margin, cur_lines ) |
| self.items.append( code ) |
| |
| elif cur_lines: |
| para = DocPara( cur_lines ) |
| self.items.append( para ) |
| |
| def dump( self, prefix = "" ): |
| if self.field: |
| print prefix + self.field + " ::" |
| prefix = prefix + "----" |
| |
| first = 1 |
| for p in self.items: |
| if not first: |
| print "" |
| p.dump( prefix ) |
| first = 0 |
| |
| def dump_lines( self, margin=0, width=60 ): |
| result = [] |
| nl = None |
| for p in self.items: |
| if nl: |
| result.append( "" ) |
| |
| result.extend( p.dump_lines( margin, width ) ) |
| nl = 1 |
| |
| return result |
| |
| |
| |
| # this regular expression is used to detect field definitions |
| # |
| re_field = re.compile( r"\s*(\w*|\w(\w|\.)*\w)\s*::" ) |
| |
| |
| |
| class DocMarkup: |
| |
| def __init__( self, tag, lines ): |
| self.tag = string.lower( tag ) |
| self.fields = [] |
| |
| cur_lines = [] |
| field = None |
| mode = 0 |
| |
| for l in lines: |
| m = re_field.match( l ) |
| if m: |
| # we detected the start of a new field definition |
| |
| # first, save the current one |
| if cur_lines: |
| f = DocField( field, cur_lines ) |
| self.fields.append( f ) |
| cur_lines = [] |
| field = None |
| |
| field = m.group( 1 ) # record field name |
| ln = len( m.group( 0 ) ) |
| l = " "*ln + l[ln:] |
| cur_lines = [l] |
| else: |
| cur_lines.append( l ) |
| |
| if field or cur_lines: |
| f = DocField( field, cur_lines ) |
| self.fields.append( f ) |
| |
| def get_name( self ): |
| try: |
| return self.fields[0].items[0].words[0] |
| |
| except: |
| return None |
| |
| def get_start( self ): |
| try: |
| result = "" |
| for word in self.fields[0].items[0].words: |
| result = result + " " + word |
| return result[1:] |
| |
| except: |
| return "ERROR" |
| |
| def dump( self, margin ): |
| print " "*margin + "<" + self.tag + ">" |
| for f in self.fields: |
| f.dump( " " ) |
| print " "*margin + "</" + self.tag + ">" |
| |
| |
| |
| class DocChapter: |
| |
| def __init__( self, block ): |
| self.block = block |
| self.sections = [] |
| if block: |
| self.name = block.name |
| self.title = block.get_markup_words( "title" ) |
| self.order = block.get_markup_words( "sections" ) |
| else: |
| self.name = "Other" |
| self.title = string.split( "Miscellaneous" ) |
| self.order = [] |
| |
| |
| |
| class DocSection: |
| |
| def __init__( self, name = "Other" ): |
| self.name = name |
| self.blocks = {} |
| self.block_names = [] # ordered block names in section |
| self.defs = [] |
| self.abstract = "" |
| self.description = "" |
| self.order = [] |
| self.title = "ERROR" |
| self.chapter = None |
| |
| def add_def( self, block ): |
| self.defs.append( block ) |
| |
| def add_block( self, block ): |
| self.block_names.append( block.name ) |
| self.blocks[block.name] = block |
| |
| def process( self ): |
| # lookup one block that contains a valid section description |
| for block in self.defs: |
| title = block.get_markup_text( "title" ) |
| if title: |
| self.title = title |
| self.abstract = block.get_markup_words( "abstract" ) |
| self.description = block.get_markup_items( "description" ) |
| self.order = block.get_markup_words( "order" ) |
| return |
| |
| def reorder( self ): |
| self.block_names = sort_order_list( self.block_names, self.order ) |
| |
| |
| |
| class ContentProcessor: |
| |
| def __init__( self ): |
| """initialize a block content processor""" |
| self.reset() |
| |
| self.sections = {} # dictionary of documentation sections |
| self.section = None # current documentation section |
| |
| self.chapters = [] # list of chapters |
| |
| def set_section( self, section_name ): |
| """set current section during parsing""" |
| if not self.sections.has_key( section_name ): |
| section = DocSection( section_name ) |
| self.sections[section_name] = section |
| self.section = section |
| else: |
| self.section = self.sections[section_name] |
| |
| def add_chapter( self, block ): |
| chapter = DocChapter( block ) |
| self.chapters.append( chapter ) |
| |
| |
| def reset( self ): |
| """reset the content processor for a new block""" |
| self.markups = [] |
| self.markup = None |
| self.markup_lines = [] |
| |
| def add_markup( self ): |
| """add a new markup section""" |
| if self.markup and self.markup_lines: |
| |
| # get rid of last line of markup if it's empty |
| marks = self.markup_lines |
| if len( marks ) > 0 and not string.strip( marks[-1] ): |
| self.markup_lines = marks[:-1] |
| |
| m = DocMarkup( self.markup, self.markup_lines ) |
| |
| self.markups.append( m ) |
| |
| self.markup = None |
| self.markup_lines = [] |
| |
| def process_content( self, content ): |
| """process a block content and return a list of DocMarkup objects |
| corresponding to it""" |
| markup = None |
| markup_lines = [] |
| first = 1 |
| |
| for line in content: |
| found = None |
| for t in re_markup_tags: |
| m = t.match( line ) |
| if m: |
| found = string.lower( m.group( 1 ) ) |
| prefix = len( m.group( 0 ) ) |
| line = " "*prefix + line[prefix:] # remove markup from line |
| break |
| |
| # is it the start of a new markup section ? |
| if found: |
| first = 0 |
| self.add_markup() # add current markup content |
| self.markup = found |
| if len( string.strip( line ) ) > 0: |
| self.markup_lines.append( line ) |
| elif first == 0: |
| self.markup_lines.append( line ) |
| |
| self.add_markup() |
| |
| return self.markups |
| |
| def parse_sources( self, source_processor ): |
| blocks = source_processor.blocks |
| count = len( blocks ) |
| for n in range( count ): |
| |
| source = blocks[n] |
| if source.content: |
| # this is a documentation comment, we need to catch |
| # all following normal blocks in the "follow" list |
| # |
| follow = [] |
| m = n+1 |
| while m < count and not blocks[m].content: |
| follow.append( blocks[m] ) |
| m = m+1 |
| |
| doc_block = DocBlock( source, follow, self ) |
| |
| def finish( self ): |
| # process all sections to extract their abstract, description |
| # and ordered list of items |
| # |
| for sec in self.sections.values(): |
| sec.process() |
| |
| # process chapters to check that all sections are correctly |
| # listed there |
| for chap in self.chapters: |
| for sec in chap.order: |
| if self.sections.has_key( sec ): |
| section = self.sections[sec] |
| section.chapter = chap |
| section.reorder() |
| chap.sections.append( section ) |
| else: |
| sys.stderr.write( "WARNING: chapter '" + |
| chap.name + "' in " + chap.block.location() + \ |
| " lists unknown section '" + sec + "'\n" ) |
| |
| # check that all sections are in a chapter |
| # |
| others = [] |
| for sec in self.sections.values(): |
| if not sec.chapter: |
| others.append( sec ) |
| |
| # create a new special chapter for all remaining sections |
| # when necessary |
| # |
| if others: |
| chap = DocChapter( None ) |
| chap.sections = others |
| self.chapters.append( chap ) |
| |
| |
| |
| class DocBlock: |
| |
| def __init__( self, source, follow, processor ): |
| processor.reset() |
| |
| self.source = source |
| self.code = [] |
| self.type = "ERRTYPE" |
| self.name = "ERRNAME" |
| self.section = processor.section |
| self.markups = processor.process_content( source.content ) |
| |
| # compute block type from first markup tag |
| try: |
| self.type = self.markups[0].tag |
| except: |
| pass |
| |
| # compute block name from first markup paragraph |
| try: |
| markup = self.markups[0] |
| para = markup.fields[0].items[0] |
| name = para.words[0] |
| m = re_identifier.match( name ) |
| if m: |
| name = m.group( 1 ) |
| self.name = name |
| except: |
| pass |
| |
| # detect new section starts |
| if self.type == "section": |
| processor.set_section( self.name ) |
| processor.section.add_def( self ) |
| |
| # detect new chapter |
| elif self.type == "chapter": |
| processor.add_chapter( self ) |
| |
| else: |
| processor.section.add_block( self ) |
| |
| # now, compute the source lines relevant to this documentation |
| # block. We keep normal comments in for obvious reasons (??) |
| source = [] |
| for b in follow: |
| if b.format: |
| break |
| for l in b.lines: |
| # we use "/* */" as a separator |
| if re_source_sep.match( l ): |
| break |
| source.append( l ) |
| |
| # now strip the leading and trailing empty lines from the sources |
| start = 0 |
| end = len( source )-1 |
| |
| while start < end and not string.strip( source[start] ): |
| start = start + 1 |
| |
| while start < end and not string.strip( source[end] ): |
| end = end - 1 |
| |
| source = source[start:end+1] |
| |
| self.code = source |
| |
| def location( self ): |
| return self.source.location() |
| |
| def get_markup( self, tag_name ): |
| """return the DocMarkup corresponding to a given tag in a block""" |
| for m in self.markups: |
| if m.tag == string.lower( tag_name ): |
| return m |
| return None |
| |
| def get_markup_name( self, tag_name ): |
| """return the name of a given primary markup in a block""" |
| try: |
| m = self.get_markup( tag_name ) |
| return m.get_name() |
| except: |
| return None |
| |
| def get_markup_words( self, tag_name ): |
| try: |
| m = self.get_markup( tag_name ) |
| return m.fields[0].items[0].words |
| except: |
| return [] |
| |
| def get_markup_text( self, tag_name ): |
| result = self.get_markup_words( tag_name ) |
| return string.join( result ) |
| |
| def get_markup_items( self, tag_name ): |
| try: |
| m = self.get_markup( tag_name ) |
| return m.fields[0].items |
| except: |
| return None |
| |
| # eof |