aboutsummaryrefslogtreecommitdiffstats
path: root/pycco
diff options
context:
space:
mode:
Diffstat (limited to 'pycco')
-rwxr-xr-xpycco219
1 files changed, 219 insertions, 0 deletions
diff --git a/pycco b/pycco
new file mode 100755
index 0000000..bc1ce3f
--- /dev/null
+++ b/pycco
@@ -0,0 +1,219 @@
+#!/usr/bin/env python
+
+# **Pycco** 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 Pycco against its own source file.
+#
+# If you install Pycco, you can run it from the command-line:
+#
+# pycco src/*.py
+#
+# ...will generate linked HTML documentation for the named source files, saving
+# it into a `docs` folder.
+#
+# To install Pycco, 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/pycco.html`
+def generate_html(source, sections):
+ title = path.basename(source)
+ dest = destination(source)
+ html = pycco_template({
+ "title": title,
+ "sections": sections,
+ "sources": sources,
+ "path": path,
+ "destination": destination
+ })
+ print "pycco = %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 Pycco supports, mapping the file extension to
+# the name of the Pygments lexer and the symbol that indicates a comment. To
+# add another language to Pycco'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": ";;" },
+ ".lua": { "name": "lua", "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*<span class="c[1]?">' + l["symbol"] + 'DIVIDER</span>\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 Pycco HTML page.
+pycco_template = template(open(path.join(path.dirname(__file__),
+ "resources/pycco.html")).read())
+
+# The CSS styles we"d like to apply to the documentation.
+pycco_styles = open(path.join(path.dirname(__file__),
+ "resources/pycco.css")).read()
+
+# The start of each Pygments highlight block.
+highlight_start = "<div class=\"highlight\"><pre>"
+
+# The end of each Pygments highlight block.
+highlight_end = "</pre></div>"
+
+# 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/pycco.css", "w")
+ css.write(pycco_styles)
+ css.close()
+
+ def next_file():
+ generate_documentation(sources.pop(0))
+ if sources:
+ next_file()
+ next_file()