| # |
| # tomarkdown.py |
| # |
| # A sub-class container of the `Formatter' class to produce Markdown. |
| # |
| # Copyright 2018 by |
| # Nikhil Ramakrishnan. |
| # |
| # This file is part of the FreeType project, and may only be used, |
| # modified, and distributed under the terms of the FreeType project |
| # license, LICENSE.TXT. By continuing to use, modify, or distribute |
| # this file you indicate that you have read the license and |
| # understand and accept it fully. |
| |
| # The parent class is contained in file `formatter.py'. |
| |
| """Subclass of `formatter` to generate Markdown. |
| |
| This module subclasses `formatter` and implements syntax-specific |
| routines to build markdown output. |
| """ |
| |
| import logging |
| import os |
| import re |
| import time |
| |
| import mistune |
| |
| from formatter import Formatter |
| import siteconfig |
| import sources |
| |
| log = logging.getLogger( __name__ ) |
| |
| #--------------------------------------------------------------- |
| # Begin initial configuration |
| |
| # Docs Config. |
| api_ref_text = "API Reference" |
| docs_author = "FreeType Contributors" |
| |
| # Breadcrumbs Navigation config. |
| md_crumbs_sep = " » " |
| |
| md_header_1 = """\ |
| [FreeType](//www.freetype.org) » \ |
| """ |
| |
| md_header_2 = """\ |
| [Docs](../) » \ |
| """ |
| |
| md_line_sep = """ |
| |
| -------------------------------\ |
| |
| """ |
| |
| # Heading Default Text. |
| md_api_ref = """\ |
| API Reference |
| """ |
| |
| # Chapter header/inter/footer. |
| chapter_header = """\ |
| ## \ |
| """ |
| |
| chapter_footer = '' |
| |
| # Synopsis text |
| section_synopsis_header = ''' |
| ## Synopsis\ |
| ''' |
| section_synopsis_footer = '' |
| |
| # Description header/footer. |
| description_header = "" |
| description_footer = "" |
| |
| # Source code extracts header/footer. |
| source_header = """ |
| <div class = "codehilite"> |
| <pre>\ |
| """ |
| source_footer = """\ |
| </pre> |
| </div>\ |
| """ |
| |
| code_header = "```" |
| code_footer = "```" |
| |
| # Source language keyword coloration and styling. |
| keyword_prefix = '<span class="keyword">' |
| keyword_suffix = '</span>' |
| |
| # HTML paragraph header and footer. |
| para_header = "<p>" |
| para_footer = "</p>" |
| |
| # General Markdown. |
| md_newline = "\n" |
| md_h1 = "# " |
| md_h2 = "## " |
| md_h3 = "### " |
| md_h4 = "<h4>" |
| md_h4_inter = "</h4>" |
| |
| md_hr = """\ |
| <hr> |
| """ |
| |
| # End of initial configuration |
| #--------------------------------------------------------------- |
| |
| def html_quote( line ): |
| """Change HTML special characters to their codes. |
| |
| Follows ISO 8859-1 Characters changed: `&`, `<` and `>`. |
| """ |
| result = line |
| if "`" not in result: |
| result = line.replace( "&", "&" ) |
| result = result.replace( "<", "<" ) |
| result = result.replace( ">", ">" ) |
| return result |
| |
| ################################################################ |
| ## |
| ## MARKDOWN FORMATTER CLASS |
| ## |
| class MdFormatter( Formatter ): |
| |
| def __init__( self, processor, project_title, file_prefix ): |
| Formatter.__init__( self, processor ) |
| |
| if file_prefix: |
| file_prefix = file_prefix + "-" |
| else: |
| file_prefix = "" |
| |
| self.headers = processor.headers |
| self.project_title = project_title |
| self.file_prefix = file_prefix |
| self.toc_filename = self.file_prefix + "toc.md" |
| self.index_filename = self.file_prefix + "index.md" |
| self.markdown = mistune.Markdown() |
| self.config = siteconfig.SiteConfig() |
| |
| self.md_index_header = ( |
| md_header_1 + md_header_2 |
| + "Global Index" |
| + md_line_sep + md_h1 |
| + project_title + md_api_ref |
| ) |
| |
| self.md_toc_header = ( |
| md_header_1 + md_header_2 |
| + "Table of Contents" |
| + md_line_sep + md_h1 |
| + project_title + md_api_ref |
| ) |
| |
| self.time_footer = ( |
| '<div class="timestamp">generated on ' |
| + time.asctime( time.gmtime() ) + " UTC" |
| + "</div>" ) |
| |
| self.columns = 3 |
| |
| self.site_name = project_title + " " + api_ref_text |
| self.site_description = api_ref_text + " Documentation for " + project_title |
| self.site_author = docs_author |
| |
| # Set site config |
| self.config.set_site_info( self.site_name, self.site_description, |
| self.site_author ) |
| # Add toc and index |
| self.config.add_single_page( "TOC", self.toc_filename ) |
| self.config.add_single_page( "Index", self.index_filename ) |
| |
| def normalize_url( self, url ): |
| # normalize url, following RFC 3986 |
| url = url.replace( "[", "(" ) |
| url = url.replace( "]", ")" ) |
| return url |
| |
| def slugify( self, name ): |
| """Slugify a cross-reference. |
| |
| Python markdown uses a similar approach to process links so we |
| need to do this in order to have valid cross-references. |
| """ |
| name = name.lower().strip() |
| name = name.replace( " ", "-") |
| return name |
| |
| def make_section_url( self, section, code = False ): |
| if code: |
| return "../" + self.file_prefix + section.name + "/index.html" |
| return self.file_prefix + section.name + ".md" |
| |
| def make_block_url( self, block, name = None, code = False ): |
| if name == None: |
| name = block.name |
| |
| name = self.slugify( name ) |
| |
| try: |
| # if it is a field def, link to its parent section |
| section_url = self.make_section_url( block.section, code ) |
| except Exception: |
| # we already have a section |
| section_url = self.make_section_url( block, code ) |
| |
| return section_url + "#" + name |
| |
| def make_chapter_url( self, chapter ): |
| chapter = ' '.join( chapter ) |
| slug_chapter = self.slugify( chapter ) |
| chapter_url = ( "[" + chapter + "](" |
| + self.toc_filename + "#" + slug_chapter + ")" |
| ) |
| return chapter_url |
| |
| def make_md_word( self, word ): |
| """Analyze a simple word to detect cross-references and markup.""" |
| # handle cross-references |
| m = sources.re_crossref.match( word ) |
| if m: |
| try: |
| name = m.group( 'name' ) |
| rest = m.group( 'rest' ) |
| block = self.identifiers[name] |
| url = self.make_block_url( block, code = True ) |
| # display `foo[bar]' as `foo' |
| name = re.sub( r'\[.*\]', '', name ) |
| # normalize url |
| url = self.normalize_url( url ) |
| try: |
| # for sections, display title |
| url = ( '‘<a href="' + url + '">' |
| + block.title + '</a>’' |
| + rest ) |
| except Exception: |
| url = ( '<a href="' + url + '">' |
| + name + '</a>' |
| + rest ) |
| |
| return url |
| except Exception: |
| # we detected a cross-reference to an unknown item |
| log.warn( "Undefined cross reference '%s'.", name ) |
| return '?' + name + '?' + rest |
| |
| return html_quote( word ) |
| |
| def make_md_para( self, words, in_html = False ): |
| """Convert words of a paragraph into tagged Markdown text. |
| |
| Also handle cross references. |
| """ |
| line = "" |
| if words: |
| line = self.make_md_word( words[0] ) |
| for word in words[1:]: |
| line = line + " " + self.make_md_word( word ) |
| # handle hyperlinks |
| line = sources.re_url.sub( r'<\1>', line ) |
| # convert '...' quotations into real left and right single quotes |
| line = re.sub( r"(^|\W)'(.*?)'(\W|$)", |
| r'\1‘\2’\3', |
| line ) |
| # convert tilde into non-breaking space |
| line = line.replace( "~", " " ) |
| |
| # Return |
| if in_html: |
| # If we are in an HTML tag, return with newline after para |
| return line + md_newline |
| # Otherwise return a Markdown paragraph |
| return md_newline + line |
| |
| def make_md_code( self, lines, lang ): |
| """Convert a code sequence to markdown.""" |
| if not lang: |
| lang = '' |
| line = code_header + lang + '\n' |
| for l in lines: |
| # NOTE Markdown REQUIRES all special chars in code blocks |
| line = line + l.rstrip() + '\n' |
| |
| return line + code_footer |
| |
| def make_md_items( self, items, in_html = False ): |
| """Convert a field's content into markdown.""" |
| lines = [] |
| for item in items: |
| if item.lines: |
| lines.append( self.make_md_code( item.lines, item.lang ) ) |
| else: |
| lines.append( self.make_md_para( item.words, in_html ) ) |
| |
| return '\n'.join( lines ) |
| |
| def print_md_items( self, items, in_html = False ): |
| content = self.make_md_items( items, in_html ) |
| if in_html: |
| # Parse markdown in content |
| content = self.markdown( content ).rstrip() |
| print( content ) |
| |
| def print_md_para( self, words, in_html = False ): |
| content = self.make_md_para( words, in_html ) |
| if in_html: |
| # Parse markdown in content |
| content = self.markdown( content ).rstrip() |
| return content |
| |
| def print_html_field( self, field ): |
| if field.name: |
| print( '<table><tr valign="top"><td><b>' |
| + field.name |
| + "</b></td><td>" ) |
| |
| print( self.make_md_items( field.items ) ) |
| |
| if field.name: |
| print( "</td></tr></table>" ) |
| |
| def source_quote( self, line, block_name = None ): |
| result = "" |
| while line: |
| m = sources.re_source_crossref.match( line ) |
| if m: |
| name = m.group( 2 ) |
| prefix = html_quote( m.group( 1 ) ) |
| length = len( m.group( 0 ) ) |
| |
| if name == block_name: |
| # this is the current block name, if any |
| result = result + prefix + '<b>' + name + '</b>' |
| # result = result + prefix + name |
| |
| # Keyword highlighting |
| elif sources.re_source_keywords.match( name ): |
| # this is a C keyword |
| result = ( result + prefix |
| + keyword_prefix + name + keyword_suffix ) |
| |
| elif name in self.identifiers: |
| # this is a known identifier |
| block = self.identifiers[name] |
| iden = block.name |
| |
| # link to a field ID if possible |
| try: |
| for markup in block.markups: |
| if markup.tag == 'values': |
| for field in markup.fields: |
| if field.name: |
| iden = name |
| |
| result = ( result + prefix |
| + '<a href="' |
| + self.make_block_url( block, iden, code = True ) |
| + '">' + name + '</a>' ) |
| except Exception: |
| # sections don't have `markups'; however, we don't |
| # want references to sections here anyway |
| result = result + html_quote( line[:length] ) |
| |
| else: |
| result = result + html_quote( line[:length] ) |
| |
| line = line[length:] |
| else: |
| result = result + html_quote( line ) |
| line = [] |
| |
| return result |
| |
| def print_md_field_list( self, fields ): |
| is_long = False |
| for field in fields: |
| # if any field name is longer than |
| # 25 chars change to long table |
| if len( field.name ) > 25: |
| is_long = True |
| break |
| # if any line has a code sequence |
| # change to long table |
| for item in field.items: |
| if item.lines: |
| is_long = True |
| break |
| if is_long: |
| print( '<table class="fields long">' ) |
| else: |
| print( '<table class="fields">' ) |
| for field in fields: |
| print( '<tr><td class="val" id="' + self.slugify( field.name ) + '">' |
| + field.name |
| + '</td><td class="desc">' ) |
| self.print_md_items( field.items, in_html = True ) |
| print( "</td></tr>" ) |
| print( "</table>" ) |
| |
| def print_md_markup( self, markup ): |
| table_fields = [] |
| for field in markup.fields: |
| if field.name: |
| # We begin a new series of field or value definitions. We |
| # record them in the `table_fields' list before outputting |
| # all of them as a single table. |
| table_fields.append( field ) |
| else: |
| if table_fields: |
| self.print_md_field_list( table_fields ) |
| table_fields = [] |
| |
| self.print_md_items( field.items ) |
| |
| if table_fields: |
| self.print_md_field_list( table_fields ) |
| |
| # |
| # formatting the index |
| # |
| def index_enter( self ): |
| print( self.md_index_header ) |
| self.index_items = {} |
| |
| def index_name_enter( self, name ): |
| block = self.identifiers[name] |
| url = self.make_block_url( block ) |
| self.index_items[name] = url |
| |
| def index_exit( self ): |
| # `block_index' already contains the sorted list of index names |
| letter = '' |
| for bname in self.block_index: |
| if letter != bname[0].upper(): |
| # print letter heading |
| letter = bname[0].upper() |
| print( '\n' + md_h3 + letter + '\n' ) |
| url = self.index_items[bname] |
| # display `foo[bar]' as `foo (bar)' |
| bname = bname.replace( "[", " (" ) |
| bname = bname.replace( "]", ")" ) |
| # normalize url |
| url = self.normalize_url( url ) |
| line = ( '[' + bname + ']' + '(' + url + ')' + ' ' ) |
| print( line ) |
| |
| # TODO Remove commented code once the above is ready |
| # count = len( self.block_index ) |
| # rows = ( count + self.columns - 1 ) // self.columns |
| |
| # print( '<table class="index">' ) |
| # for r in range( rows ): |
| # line = "<tr>" |
| # for c in range( self.columns ): |
| # i = r + c * rows |
| # if i < count: |
| # bname = self.block_index[r + c * rows] |
| # url = self.index_items[bname] |
| # # display `foo[bar]' as `foo (bar)' |
| # bname = bname.replace( "[", " (" ) |
| # bname = bname.replace( "]", ")" ) |
| # # normalize url |
| # url = self.normalize_url( url ) |
| # line = ( line + '<td><a href="' + url + '">' |
| # + bname + '</a></td>' ) |
| # else: |
| # line = line + '<td></td>' |
| # line = line + "</tr>" |
| # print( line ) |
| |
| # print( "</table>" ) |
| |
| print( md_line_sep ) |
| print( self.time_footer ) |
| |
| self.index_items = {} |
| |
| def index_dump( self, index_filename = None ): |
| if index_filename == None: |
| index_filename = self.file_prefix + "index.md" |
| |
| Formatter.index_dump( self, index_filename ) |
| |
| # |
| # Formatting the table of contents and |
| # config file for MkDocs. |
| # |
| def toc_enter( self ): |
| print( self.md_toc_header ) |
| print( "# Table of Contents" ) |
| |
| def toc_chapter_enter( self, chapter ): |
| print( chapter_header + " ".join( chapter.title ) + md_newline ) |
| print( '<table class="toc">' ) |
| # add a chapter |
| self.config.start_chapter( " ".join( chapter.title ) ) |
| |
| def toc_section_enter( self, section ): |
| print( '<tr><td class="link">' |
| + '<a href="' |
| + self.make_section_url( section, code = True ) + '">' |
| + section.title + '</a></td><td class="desc">' ) |
| print( self.print_md_para( section.abstract, in_html = True ) ) |
| # add section to chapter |
| self.config.add_chapter_page( section.title, |
| self.make_section_url( section ) ) |
| |
| def toc_section_exit( self, section ): |
| print( "</td></tr>" ) |
| |
| def toc_chapter_exit( self, chapter ): |
| print( "</table>" ) |
| #print( chapter_footer ) |
| # End the chapter |
| self.config.end_chapter() |
| |
| def toc_index( self, index_filename ): |
| print( chapter_header |
| + '[Global Index](' + index_filename + ')' |
| ) |
| |
| def toc_exit( self ): |
| print( md_line_sep ) |
| print( self.time_footer ) |
| # Build and flush MkDocs config |
| self.config.build_config() |
| |
| def toc_dump( self, toc_filename = None, index_filename = None ): |
| if toc_filename == None: |
| toc_filename = self.file_prefix + "toc.md" |
| |
| if index_filename == None: |
| index_filename = self.file_prefix + "index.md" |
| |
| Formatter.toc_dump( self, toc_filename, index_filename ) |
| |
| # |
| # formatting sections |
| # |
| def section_enter( self, section ): |
| if section.chapter: |
| print( md_header_1 + md_header_2 |
| + self.make_chapter_url( section.chapter.title ) |
| + md_crumbs_sep + section.title |
| + md_line_sep ) |
| else: |
| # this should never happen! |
| log.warn( "No chapter name for Section '%s'.", section.title ) |
| |
| # Print section title |
| print( md_h1 + section.title ) |
| |
| # print section synopsis |
| print( section_synopsis_header ) |
| #print( section_synopsis_footer ) |
| |
| #print( description_header ) |
| print( self.make_md_items( section.description ) ) |
| print( description_footer ) |
| |
| def block_enter( self, block ): |
| |
| # place anchor if needed |
| if block.name: |
| url = block.name |
| # display `foo[bar]' as `foo' |
| name = re.sub( r'\[.*\]', '', block.name ) |
| # normalize url |
| url = self.normalize_url( url ) |
| print( md_h2 + name + md_newline ) |
| |
| # dump the block C source lines now |
| if block.code: |
| header = '' |
| for f in self.headers.keys(): |
| header_filename = os.path.normpath( block.source.filename ) |
| if header_filename.find( os.path.normpath( f ) ) >= 0: |
| header = self.headers[f] + ' (' + f + ')' |
| break |
| |
| # Warn if header macro not found |
| # if not header: |
| # log.warn( |
| # "No header macro for" |
| # " '%s'.", block.source.filename ) |
| |
| if header: |
| print( 'Defined in ' + header + '.' ) |
| |
| print( source_header ) |
| for l in block.code: |
| print( self.source_quote( l, block.name ) ) |
| print( source_footer ) |
| |
| def markup_enter( self, markup, block ): |
| if markup.tag == "description": |
| print( description_header ) |
| else: |
| print( md_h4 + markup.tag + md_h4_inter ) |
| |
| self.print_md_markup( markup ) |
| |
| def markup_exit( self, markup, block ): |
| if markup.tag == "description": |
| print( description_footer ) |
| else: |
| print( "" ) |
| |
| def block_exit( self, block ): |
| print( md_hr ) |
| |
| def section_exit( self, section ): |
| pass |
| |
| def section_dump_all( self ): |
| log.debug( "Building markdown pages for sections." ) |
| for section in self.sections: |
| self.section_dump( section, |
| self.file_prefix + section.name + '.md' ) |
| |
| # eof |