#!/usr/bin/env python # **Pocco** is a Python port of [Docco](http://jashkenas.github.com/docco/ ): # the original quick-and-dirty, hundred-line-long, literate-programming-style # documentation generator. It produces HTML that displays your comments # alongside your code. Comments are passed through # [Markdown](http://daringfireball.net/projects/markdown/syntax), and code is # passed through [Pygments](http://pygments.org/) syntax highlighting. This # page is the result of running Pocco against its own source file. # # If you install Pocco, you can run it from the command-line: # # pocco src/*.py # # ...will generate linked HTML documentation for the named source files, saving # it into a `docs` folder. # # To install Pocco, simply # # sudo setup.py install # #### Main Documentation Generation Functions # Generate the documentation for a source file by reading it in, splitting it # up into comment/code sections, highlighting them for the appropriate language, # and merging them into an HTML template. def generate_documentation(source): fh = open(source, "r") sections = parse(source, fh.read()) highlight(source, sections) generate_html(source, sections) # Given a string of source code, parse out each comment and the code that # follows it, and create an individual **section** for it. # Sections take the form: # # { "docs_text": ..., # "docs_html": ..., # "code_text": ..., # "code_html": ..., # "num": ... # } # def parse(source, code): lines = code.split("\n") sections = [] language = get_language(source) has_code = docs_text = code_text = "" if lines[0].startswith("#!"): lines.pop(0) def save(docs, code): sections.append({ "docs_text": docs, "code_text": code }) for line in lines: if re.match(language["comment_matcher"], line): if has_code: save(docs_text, code_text) has_code = docs_text = code_text = '' docs_text += re.sub(language["comment_matcher"], "", line) + "\n" else: has_code = True code_text += line + '\n' save(docs_text, code_text) return sections # Highlights a single chunk of code using the **Pygments** module, and runs the # text of its corresponding comment through **Markdown**. # # We process the entire file in a single call to Pygments by inserting little # marker comments between each section and then splitting the result string # wherever our markers occur. def highlight(source, sections): language = get_language(source) output = pygments.highlight(language["divider_text"].join(section["code_text"] for section in sections), language["lexer"], formatters.get_formatter_by_name("html")) output = output.replace(highlight_start, "").replace(highlight_end, "") fragments = re.split(language["divider_html"], output) for i, section in enumerate(sections): section["code_html"] = highlight_start + shift(fragments, "") + highlight_end section["docs_html"] = markdown(section["docs_text"]) section["num"] = i # Once all of the code is finished highlighting, we can generate the HTML file # and write out the documentation. Pass the completed sections into the template # found in `resources/pocco.html` def generate_html(source, sections): title = path.basename(source) dest = destination(source) html = pocco_template({ "title": title, "sections": sections, "sources": sources, "path": path, "destination": destination }) print "pocco = %s -> %s" % (source, dest) fh = open(dest, "w") fh.write(html) fh.close() #### Helpers & Setup # Import our external dependencies. import pygments import pystache import re import sys from markdown import markdown from os import path from pygments import lexers, formatters from subprocess import Popen, PIPE # A list of the languages that Pocco supports, mapping the file extension to # the name of the Pygments lexer and the symbol that indicates a comment. To # add another language to Pocco's repertoire, add it here. languages = { ".coffee": { "name": "coffee-script", "symbol": "#" }, ".js": { "name": "javascript", "symbol": "//" }, ".rb": { "name": "ruby", "symbol": "#" }, ".py": { "name": "python", "symbol": "#" }, ".scm": { "name": "scheme", "symbol": ";;" }, } # Build out the appropriate matchers and delimiters for each language. for ext, l in languages.items(): # Does the line begin with a comment? l["comment_matcher"] = re.compile(r"^\s*" + l["symbol"] + "\s?") # The dividing token we feed into Pygments, to delimit the boundaries between # sections. l["divider_text"] = "\n" + l["symbol"] + "DIVIDER\n" # The mirror of `divider_text` that we expect Pygments to return. We can split # on this to recover the original sections. l["divider_html"] = re.compile(r'\n*' + l["symbol"] + 'DIVIDER\n*') # Get the Pygments Lexer for this language. l["lexer"] = lexers.get_lexer_by_name(l["name"]) # Get the current language we're documenting, based on the extension. def get_language(source): try: return languages[ source[source.rindex("."):] ] except ValueError: source = open(source, "r") code = source.read() source.close() lang = lexers.guess_lexer(code).name.lower() for l in languages.values(): if l["name"] == lang: return l else: raise ValueError("Can't figure out the language!") # Compute the destination HTML path for an input source file path. If the source # is `lib/example.py`, the HTML will be at `docs/example.html` def destination(filepath): try: name = filepath.replace(filepath[ filepath.rindex("."): ], "") except ValueError: name = filepath return "docs/" + path.basename(name) + ".html" # Shift items off the front of the `list` until it is empty, then return # `default`. def shift(list, default): try: return list.pop(0) except IndexError: return default # Ensure that the destination directory exists. def ensure_directory(): Popen(["mkdir", "-p", "docs"]).wait() def template(source): return lambda context: pystache.render(source, context) # Create the template that we will use to generate the Pocco HTML page. pocco_template = template(open(path.join(path.dirname(__file__), "resources/pocco.html")).read()) # The CSS styles we"d like to apply to the documentation. pocco_styles = open(path.join(path.dirname(__file__), "resources/pocco.css")).read() # The start of each Pygments highlight block. highlight_start = "
"

# The end of each Pygments highlight block.
highlight_end = "
" # Run the script. # For each source file passed in as an argument, generate the documentation. if __name__ == "__main__": sources = list(sys.argv[1:]) sources.sort() if sources: ensure_directory() css = open("docs/pocco.css", "w") css.write(pocco_styles) css.close() def next_file(): generate_documentation(sources.pop(0)) if sources: next_file() next_file()