docco.coffee

Docco is a 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, and code is passed through Pygments syntax highlighting. This page is the result of running Docco against its own source file.

If you install Docco, you can run it from the command-line:

docco src/*.coffee

...will generate an HTML documentation page for each of the named source files, with a menu linking to the other pages, saving it into a docs folder.

The source for Docco is available on GitHub, and released under the MIT license.

To install Docco, first make sure you have Node.js, Pygments (install the latest dev version of Pygments from its Mercurial repo), and CoffeeScript. Then, with NPM:

sudo npm install -g docco

Docco can be used to process CoffeeScript, JavaScript, Ruby, Python, or TeX files. Only single-line comments are processed -- block comments are ignored.

Partners in Crime:

  • If Node.js doesn't run on your platform, or you'd prefer a more convenient package, get Ryan Tomayko's Rocco, the Ruby port that's available as a gem.

  • If you're writing shell scripts, try Shocco, a port for the POSIX shell, also by Mr. Tomayko.

  • If Python's more your speed, take a look at Nick Fitzgerald's Pycco.

  • For Clojure fans, Fogus's Marginalia is a bit of a departure from "quick-and-dirty", but it'll get the job done.

  • Lua enthusiasts can get their fix with Robert Gieseke's Locco.

  • And if you happen to be a .NET aficionado, check out Don Wilson's Nocco.

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.

generateDocumentation = (source, config, callback) ->
  fs.readFile source, (error, buffer) ->
    throw error if error
    code = buffer.toString()
    sections = parse source, code
    highlight source, sections, ->
      generateHtml source, sections, config
      callback()

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:

{
  docsText: ...
  docsHtml: ...
  codeText: ...
  codeHtml: ...
}
parse = (source, code) ->
  lines    = code.split '\n'
  sections = []
  language = getLanguage source
  hasCode  = docsText = codeText = ''

  save = (docsText, codeText) ->
    sections.push {docsText, codeText}

  for line in lines
    if line.match(language.commentMatcher) and not line.match(language.commentFilter)
      if hasCode
        save docsText, codeText
        hasCode = docsText = codeText = ''
      docsText += line.replace(language.commentMatcher, '') + '\n'
    else
      hasCode = yes
      codeText += line + '\n'
  save docsText, codeText
  sections

Highlights a single chunk of CoffeeScript code, using Pygments over stdio, and runs the text of its corresponding comment through Markdown, using Showdown.js.

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.

highlight = (source, sections, callback) ->
  language = getLanguage source
  pygments = spawn 'pygmentize', [
    '-l', language.name,
    '-f', 'html',
    '-O', 'encoding=utf-8,tabsize=2'
  ]
  output   = ''
  
  pygments.stderr.on 'data',  (error)  ->
    console.error error.toString() if error
    
  pygments.stdin.on 'error',  (error)  ->
    console.error 'Could not use Pygments to highlight the source.'
    process.exit 1
    
  pygments.stdout.on 'data', (result) ->
    output += result if result
    
  pygments.on 'exit', ->
    output = output.replace(highlightStart, '').replace(highlightEnd, '')
    fragments = output.split language.dividerHtml
    for section, i in sections
      section.codeHtml = highlightStart + fragments[i] + highlightEnd
      section.docsHtml = showdown.makeHtml section.docsText
    callback()
    
  if pygments.stdin.writable
    text = (section.codeText for section in sections)
    pygments.stdin.write text.join language.dividerText
    pygments.stdin.end()
  

Once all of the code is finished highlighting, we can generate the HTML file by passing the completed sections into the template, and then writing the file to the specified output path.

generateHtml = (source, sections, config) ->
  destination = (filepath) ->
    path.join(config.output, path.basename(filepath, path.extname(filepath)) + '.html')   
  title = path.basename source
  dest  = destination source
  html  = config.doccoTemplate {
    title      : title, 
    sections   : sections, 
    sources    : config.sources, 
    path       : path, 
    destination: destination
    css        : path.basename(config.css)
  }
  console.log "docco: #{source} -> #{dest}"
  fs.writeFileSync dest, html

Helpers & Setup

Require our external dependencies, including Showdown.js (the JavaScript implementation of Markdown).

fs       = require 'fs'
path     = require 'path'
showdown = require('./../vendor/showdown').Showdown
{spawn, exec} = require 'child_process'
commander = require 'commander'

Read resource file and return its content.

getResource = (name) ->
  fullPath = path.join __dirname, '..', 'resources', name
  fs.readFileSync(fullPath).toString()

Languages are stored in JSON format in the file resources/languages.json Each item maps the file extension to the name of the Pygments lexer and the symbol that indicates a comment. To add a new language, modify the file.

languages = JSON.parse getResource 'languages.json'

Build out the appropriate matchers and delimiters for each language.

for ext, l of languages

Does the line begin with a comment?

  l.commentMatcher = ///^\s*#{l.symbol}\s?///

Ignore hashbangs and interpolations...

  l.commentFilter = /(^#![/]|^\s*#\{)/

The dividing token we feed into Pygments, to delimit the boundaries between sections.

  l.dividerText = "\n#{l.symbol}DIVIDER\n"

The mirror of dividerText that we expect Pygments to return. We can split on this to recover the original sections. Note: the class is "c" for Python and "c1" for the other languages

  l.dividerHtml = ///\n*<span\sclass="c1?">#{l.symbol}DIVIDER<\/span>\n*///

Get the current language we're documenting, based on the extension.

getLanguage = (source) -> languages[path.extname(source)]

Ensure that the destination directory exists.

ensureDirectory = (dir, callback) ->
  exec "mkdir -p #{dir}", -> callback()

Micro-templating, originally by John Resig, borrowed by way of Underscore.js.

template = (str) ->
  new Function 'obj',
    'var p=[],print=function(){p.push.apply(p,arguments);};' +
    'with(obj){p.push(\'' +
    str.replace(/[\r\t\n]/g, " ")
       .replace(/'(?=[^<]*%>)/g,"\t")
       .split("'").join("\\'")
       .split("\t").join("'")
       .replace(/<%=(.+?)%>/g, "',$1,'")
       .split('<%').join("');")
       .split('%>').join("p.push('") +
       "');}return p.join('');"

The start of each Pygments highlight block.

highlightStart = '<div class="highlight"><pre>'

The end of each Pygments highlight block.

highlightEnd   = '</pre></div>'

Extract the docco version from package.json

version = JSON.parse(fs.readFileSync("#{__dirname}/../package.json")).version

Default configuration options.

defaults =
  template: "#{__dirname}/../resources/docco.jst"
  css     : "#{__dirname}/../resources/resources/docco.css"
  output  : "docs/"

Run from Commandline

  

Run Docco from a set of command line arguments.

  1. Parse command line using Commander JS.
  2. Document sources, or print the usage help if none are specified.
run = (args=process.argv) ->
  commander.version(version)
    .usage("[options] <filePattern ...>")
    .option("-c, --css [file]","use a custom css file",defaults.css)
    .option("-o, --output [path]","use a custom output path",defaults.output)
    .option("-t, --template [file]","use a custom .jst template",defaults.template)
    .parse(args)
    .name = "docco"
  if commander.args.length
    document(commander.args.slice(),commander)
  else
    console.log commander.helpInformation()

Document Sources

Run Docco over a list of sources with the given options.

  1. Construct config to use by taking defaults first, then merging in options
  2. Generate the resolved source list, filtering out unknown types.
  3. Load the specified template and css files.
  4. Ensure the output path is created, write out the CSS file, document each source, and invoke the completion callback if it is specified.
document = (sources,options={},callback=null) ->
  config = {}
  config[key] = defaults[key] for key,value of defaults
  config[key] = value for key,value of options if key of defaults

  resolved = []
  resolved = resolved.concat(resolveSource(src)) for src in sources
  config.sources = resolved.filter((source) -> getLanguage source).sort()
  console.log "docco: skipped unknown type (#{m})" for m in resolved when m not in config.sources  
  
  config.doccoTemplate = template fs.readFileSync(config.template).toString()
  doccoStyles = fs.readFileSync(config.css).toString()

  ensureDirectory config.output, ->
    fs.writeFileSync path.join(config.output,path.basename(config.css)), doccoStyles
    files = config.sources.slice()
    nextFile = -> 
      callback() if callback? and not files.length
      generateDocumentation files.shift(), config, nextFile if files.length
    nextFile()

Resolve Wildcard Source Inputs

Resolve a wildcard source input to the files it matches.

  1. If the input contains no wildcard characters, just return it.
  2. Convert the wildcard match to a regular expression, and return an array of files in the path that match it.
resolveSource = (source) ->
  return source if not source.match(/([\*\?])/)
  regex_str = path.basename(source)
    .replace(/\./g, "\\$&")
    .replace(/\*/,".*")
    .replace(/\?/,".")
  regex = new RegExp('^(' + regex_str + ')$')
  file_path = path.dirname(source)
  files = fs.readdirSync file_path
  return (path.join(file_path,file) for file in files when file.match regex)

Exports

Information about docco, and functions for programatic usage.

exports[key] = value for key, value of {
  run           : run
  document      : document
  parse         : parse
  resolveSource : resolveSource
  version       : version
  defaults      : defaults
  languages     : languages
}