Docco is a quick-and-dirty documentation generator, written in Literate CoffeeScript. It produces an HTML document that displays your comments intermingled with your code. All prose is passed through Markdown, and code is passed through Highlight.js syntax highlighting. This page is the result of running Docco against its own source file.
-
Install Docco with npm:
sudo npm install -g docco
-
Run it against your code:
docco src/*.coffee
There is no "Step 3". This will generate an HTML page for each of the named
source files, with a menu linking to the other pages, saving the whole mess
into a docs
folder (configurable).
The Docco source is available on GitHub, and is released under the Lil License.
Docco can be used to process code written in any programming language. If it
doesn't handle your favorite yet, feel free to
add it to the list.
Finally, the "literate" style of any
language listed in languages.json
is also supported — just tack an .md
extension on the end:
.coffee.md
, .py.md
, and so on. Also get usable source code by adding the
--source
option while specifying a directory for the files.
By default only single-line comments are processed, block comments may be included
by passing the -b
flag to Docco.
-
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. (WARNING: project seems currently broken and apparently abandoned.)
-
If you're writing shell scripts, try Shocco, a port for the POSIX shell, also by Mr. Tomayko. (WARNING: project seems currently broken and apparently abandoned.)
-
If Python is more your speed, take a look at Pycco, originally developed by Nick Fitzgerald and now maintained by the pycco-docs community.
-
For Clojure fans, Fogus's Marginalia is a bit of a departure from "quick-and-dirty", but it'll get the job done.
-
There's a Go port called Gocco, written by Nikhil Marathe.
-
For all you PHP buffs out there, Fredi Bach's sourceMakeup (we'll let the faux pas with respect to our naming scheme slide), should do the trick nicely.
-
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.
-
Going further afield from the quick-and-dirty, Groc is a CoffeeScript fork of Docco that adds a searchable table of contents, and aims to gracefully handle large projects with complex hierarchies of code.
Note that not all ports will support all Docco features ... yet.
Generate the documentation for our configured source file by copying over static assets, reading all the source files in, splitting them up into prose+code sections, highlighting each file in the appropriate language, printing them out in an HTML template, and writing plain code files where instructed.
document = (options = {}, user_callback) ->
config = configure options
source_infos = []
fs.mkdirsSync config.output
fs.mkdirsSync config.source if config.source
callback = (error) ->
if error
user_callback error if user_callback
throw error
if user_callback
user_callback null, { source_infos, config }
copyAsset = (file, callback) ->
return callback() unless fs.existsSync file
fs.copy file, path.join(config.output, path.basename(file)), callback
complete = ->
copyAsset config.css, (error) ->
return callback error if error
return copyAsset config.public, callback
files = config.sources.slice()
nextFile = ->
source = files.shift()
fs.readFile source, (error, buffer) ->
return callback error if error
code = buffer.toString()
sections = parse source, code, config
format source, sections, config
The title of the file is either the first heading in the prose, or the name of the source file.
firstSection = _.find sections, (section) ->
section.docsText.length > 0
first = marked.lexer(firstSection.docsText)[0] if firstSection
hasTitle = first and first.type is 'heading' and first.depth is 1
title = if hasTitle then first.text else path.basename source
source_infos.push({
source: source,
hasTitle: hasTitle,
title: title,
sections: sections
})
if files.length then nextFile() else outputFiles()
When we have finished all preparations (such as extracting a title for each file), we produce all output files.
We have collected all titles before outputting the individual files to give the template access to all sources' titles for rendering, e.g. when the template needs to produce a TOC with each file.
outputFiles = ->
for info, i in source_infos
write info.source, i, source_infos, config
outputCode info.source, info.sections, i, source_infos, config
complete()
Start processing all sources and producing the corresponding files for each:
if files.length then nextFile() else outputFiles()
Given a string of source code, parse out each block of prose and the code that
follows it — by detecting which is which, line by line — and then create an
individual section for it. Each section is an object with docsText
and
codeText
properties, and eventually docsHtml
and codeHtml
as well.
parse = (source, code, config = {}) ->
lines = code.split '\n'
sections = []
lang = getLanguage source, config
hasCode = docsText = codeText = ''
param = ''
in_block = 0
ignore_this_block = 0
save = ->
sections.push {docsText, codeText}
hasCode = docsText = codeText = ''
Our quick-and-dirty implementation of the literate programming style. Simply invert the prose and code relationship on a per-line basis, and then continue as normal below.
Note: "Literate markdown" is an exception here as it's basically the reverse.
if lang.literate and lang.name != 'markdown'
isText = maybeCode = yes
for line, i in lines
lines[i] = if maybeCode and match = /^([ ]{4}|[ ]{0,3}\t)/.exec line
isText = no
line[match[0].length..]
else if maybeCode = /^\s*$/.test line
if isText then lang.symbol else ''
else
isText = yes
lang.symbol + ' ' + line
Iterate over the source lines, and separate out single/block comments from code chunks.
for line in lines
if in_block
++in_block
raw_line = line
If we're not in a block comment, and find a match for the start of one, eat the tokens, and note that we're now in a block.
if not in_block and config.blocks and lang.blocks and line.match(lang.commentEnter)
line = line.replace(lang.commentEnter, '')
Make sure this is a comment that we actually want to process; if not, treat it as code
in_block = 1
if lang.commentIgnore and line.match(lang.commentIgnore)
ignore_this_block = 1
Process the line, marking it as docs if we're in a block comment, or we find a single-line comment marker.
single = (not in_block and lang.commentMatcher and line.match(lang.commentMatcher) and not line.match(lang.commentFilter))
If there's a single comment, and we're not in a block, eat the comment token.
if single
line = line.replace(lang.commentMatcher, '')
Make sure this is a comment that we actually want to process; if not, treat it as code
if lang.commentIgnore and line.match(lang.commentIgnore)
ignore_this_block = 1
Prepare the line further when it is (part of) a comment line.
if in_block or single
If we're in a block comment and we find the end of it in the line, eat the end token, and note that we're no longer in the block.
if in_block and line.match(lang.commentExit)
line = line.replace(lang.commentExit, '')
in_block = -1
If we're in a block comment and are processing comment line 2 or further, eat the optional comment prefix (for C style comments, that would generally be a single '*', for example).
if in_block > 1 and lang.commentNext
line = line.replace(lang.commentNext, '');
If we happen upon a JavaDoc @param
parameter, then process that item.
if lang.commentParam
param = line.match(lang.commentParam);
if param
line = line.replace(param[0], '\n' + '<b>' + param[1] + '</b>');
if not ignore_this_block and (in_block or single)
If we have code text, and we're entering a comment, store off the current docs and code, then start a new section.
save() if hasCode
docsText += line + '\n'
save() if /^(---+|===+)$/.test line or in_block == -1
else
hasCode = yes
if config.indent
oldLen = 0
while oldLen != line.length
oldLen = line.length
line = line.replace(/^(\x20*)\t/, '$1' + config.indent)
codeText += line + '\n'
Reset in_block
when we have reached the end of the comment block.
if in_block == -1
in_block = 0
Reset ignore_this_block
when we have reached the end of the comment block or single comment line.
if not in_block
ignore_this_block = 0
Save the final section, if any, and return the sections array.
save()
sections
To format and highlight the now-parsed sections of code, we use Highlight.js over stdio, and run the text of their corresponding comments through Markdown, using Marked.
format = (source, sections, config) ->
language = getLanguage source, config
Pass any user defined options to Marked if specified via command line option, otherwise revert use the default configuration.
markedOptions = config.marked_options
marked.setOptions markedOptions
Tell Marked how to highlight code blocks within comments, treating that code as either the language specified in the code block or the language of the file if not specified.
marked.setOptions {
highlight: (code, lang) ->
lang or= language.name
if highlightjs.getLanguage(lang)
highlightjs.highlight(lang, code).value
else
console.warn "docco: couldn't highlight code block with unknown language '#{lang}' in #{source}"
code
}
Also instruct Marked to run a preliminary scan of all the chunks first, where we want to collect all link references. Only in the second run will we then require Marked to produce the final HTML rendered output.
We have to execute this 2-phase process to ensure that any input which includes MarkDown link references is processed properly: without the initial scan any link reference defined later (near the end of the input document) will be unknown to document chunks near the top of the input document.
marked.setOptions {
linksCollector: {}
execPrepPhaseOnly: true
}
Process each chunk (phase 1):
-
both the code and text blocks are stripped of trailing empty lines
-
the code block is marked up by highlighted to show a nice HTML rendition of the code
-
the text block is fed to Marked for an initial scan
for section, i in sections if language.name == 'markdown' if language.literate code = section.codeText section.codeText = code = code.replace(/\s+$/, '') section.codeHtml = marked(code) doc = section.docsText section.docsText = doc = doc.replace(/\s+$/, '') marked(doc) else section.codeHtml = '' code = section.codeText section.codeText = code = code.replace(/\s+$/, '') marked(code) else code = section.codeText section.codeText = code = code.replace(/\s+$/, '') try code = highlightjs.highlight(language.name, code).value catch err throw err unless config.ignore code = section.codeText section.codeHtml = "<div class='highlight'><pre>#{code}</pre></div>" doc = section.docsText section.docsText = doc = doc.replace(/\s+$/, '') marked(doc)
Process each chunk (phase 2):
-
the text block is fed to Marked to turn it into HTML
marked.setOptions { execPrepPhaseOnly: false } for section, i in sections if language.name == 'markdown' if language.literate doc = section.docsText section.docsHtml = marked(doc) else code = section.codeText section.docsHtml = marked(code) else doc = section.docsText section.docsHtml = marked(doc)
Once all of the code has finished highlighting, we can write the resulting documentation file by passing the completed HTML sections into the template, and rendering it to the specified output path.
write = (source, title_idx, source_infos, config) ->
destination = (file) ->
make_destination config.output, config.separator, file, '.html', config.keepext, config
destfile = destination source
relative = (srcfile) ->
to = path.dirname(path.resolve(srcfile))
dstfile = destination srcfile
from = path.dirname(path.resolve(dstfile))
path.join(path.relative(from, to), path.basename(srcfile))
css = if config.css then relative path.join(config.output, path.basename(config.css)) else null
html = config.template {
sources: config.sources
titles: source_infos.map (info) ->
info.title
css
title: source_infos[title_idx].title
hasTitle: source_infos[title_idx].hasTitle
sections: source_infos[title_idx].sections
path
destination
relative
language: getLanguage source, config
}
console.log "docco: #{source} -> #{destfile}"
fs.mkdirsSync path.dirname(destfile)
fs.writeFileSync destfile, html
source_infos[title_idx].destDocFile = destfile
Print out the consolidated code sections parsed from the source file in to another file. No documentation will be included in the new file.
outputCode = (source, sections, title_idx, source_infos, config) ->
lang = getLanguage source, config
if config.source
destfile = make_destination config.source, config.separator, source, lang.source, false, config
code = _.pluck(sections, 'codeText').join '\n'
code = code.trim().replace /(\n{2,})/g, '\n\n'
console.log "docco: #{source} -> #{destfile}"
fs.mkdirsSync path.dirname(destfile)
fs.writeFileSync destfile, code
source_infos[title_idx].destCodeFile = destfile
To help us produce decent file names and paths for all inputs, we define a few helper functions:
It should not matter if we are running on Windows, Unix or some other platform: we unify all paths to a single UNIXy format which can be processed easily everywhere (Windows accepts both native and UNIX path separators).
normalize = (path) ->
path.replace(/[\\\/]/g, '/')
We construct a suitable filename/path for each document by prepending it with the specified relative path while using the separator specified on the command line. (The default separator ('-' dash) is used to flatten the directory tree when we process a directory tree all at once.)
qualifiedName = (file, separator, extension, keep_ext, config) ->
cwd = if config and config.cwd then config.cwd else process.cwd()
file = normalize(file)
nameParts = path.dirname(file).replace(normalize(cwd), '').split('/')
nameParts.shift() while nameParts[0] is '' or nameParts[0] is '.' or nameParts[0] is '..'
nameParts.push(if keep_ext then path.basename(file) else path.basename(file, path.extname(file)))
nameParts.join(separator) + extension
make_destination = (basepath, separator, file, extension, keep_ext, config) ->
path.join basepath, qualifiedName(file, separator, extension, keep_ext, config)
Default configuration options. All of these may be extended by user-specified options.
defaults =
sources: []
layout: 'parallel'
output: 'docs'
template: null
css: null
extension: null
languages: {}
source: null
cwd: process.cwd()
separator: '-'
blocks: false
marked_options: {
gfm: true,
tables: true,
breaks: false,
pedantic: false,
sanitize: false,
smartLists: true,
smartypants: yes,
langPrefix: 'language-',
highlight: (code, lang) ->
code
}
ignore: false
keepext: false
tabSize: null
indent: null
Configure this particular run of Docco. We might use a passed-in external template, one of the built-in layouts, or an external layout. We only attempt to process source files for languages for which we have definitions.
configure = (options) ->
config = _.extend {}, defaults, _.pick(options, _.keys(defaults)...)
config.languages = buildMatchers config.languages
Determine what the indent should be if the user has supplied a custom tab-size on the command line.
if config.tabSize
config.indent = Array(parseInt(config.tabSize) + 1).join(' ')
The user is able to override the layout file used with the --template
parameter.
In this case, it is also necessary to explicitly specify a stylesheet file.
These custom templates are compiled exactly like the predefined ones, but the public
folder
is only copied for the latter.
if options.template
unless options.css
console.warn "docco: no stylesheet file specified"
config.layout = null
else
dir = config.layout = if fs.existsSync path.join __dirname, 'resources', config.layout then path.join __dirname, 'resources', config.layout else path.join process.cwd(), config.layout;
config.public = path.join dir, 'public' if fs.existsSync path.join dir, 'public'
config.template = path.join dir, 'docco.jst'
config.css = options.css or path.join dir, 'docco.css'
config.template = _.template fs.readFileSync(config.template).toString()
When the user specifies custom Marked options in a (JSON-formatted) configuration file, mix those options which our defaults such that each default option remains active when it has not been explicitly overridden by the user.
if options.marked_options
config.marked_options = _.extend config.marked_options, JSON.parse fs.readFileSync(options.marked_options)
if options.args
config.sources = options.args.filter((source) ->
lang = getLanguage source, config
console.warn "docco: skipped unknown type (#{path.basename source})" unless lang
lang
).sort()
config
Require our external dependencies.
_ = require 'underscore'
fs = require 'fs-extra'
path = require 'path'
marked = require 'marked'
commander = require 'commander'
highlightjs = require '@gerhobbelt/highlight.js'
Languages are stored in JSON in the file resources/languages.json
.
Each item maps the file extension to the name of the language and the
symbol
that indicates a line comment. To add support for a new programming
language to Docco, just add it to the file.
languages = JSON.parse fs.readFileSync(path.join(__dirname, 'resources', 'languages.json'))
Build out the appropriate matchers and delimiters for each language.
buildMatchers = (languages) ->
for ext, l of languages
Does the line begin with a comment?
if (l.symbol)
l.commentMatcher = ///^\s*#{l.symbol}\s?///
Support block comment parsing?
if l.enter and l.exit
l.blocks = true
l.commentEnter = new RegExp(l.enter)
l.commentExit = new RegExp(l.exit)
if (l.next)
l.commentNext = new RegExp(l.next)
if l.param
l.commentParam = new RegExp(l.param)
Ignore hashbangs and interpolations...
l.commentFilter = /(^#![/]|^\s*#\{)/
We ignore any comments which start with a colon ':' - these will be included in the code as is.
l.commentIgnore = new RegExp(/^:/)
languages
languages = buildMatchers languages
A function to get the current language we're documenting, based on the
file extension. Detect and tag "literate" .ext.md
variants.
getLanguage = (source, config) ->
ext = config.extension or path.extname(source) or path.basename(source)
lang = config.languages?[ext] or languages[ext] or languages['text']
if lang
if lang.name is 'markdown'
codeExt = path.extname(path.basename(source, ext))
if codeExt and codeLang = config.languages?[codeExt] or languages[codeExt] or languages['text']
lang = _.extend {}, codeLang, {literate: yes, source: ''}
else if not lang.source
lang.source = ext
lang
Keep it DRY. Extract the docco version from package.json
version = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'))).version
Finally, let's define the interface to run Docco from the command line. Parse options using Commander.
run = (args = process.argv) ->
c = defaults
commander.version(version)
.usage('[options] files')
.option('-L, --languages [file]', 'use a custom languages.json', _.compose JSON.parse, fs.readFileSync)
.option('-l, --layout [name]', 'choose a layout (parallel, linear, pretty or classic) or external layout', c.layout)
.option('-o, --output [path]', 'output to a given folder', c.output)
.option('-c, --css [file]', 'use a custom css file', c.css)
.option('-t, --template [file]', 'use a custom .jst template', c.template)
.option('-b, --blocks', 'parse block comments where available', c.blocks)
.option('-e, --extension [ext]', 'assume a file extension for all inputs', c.extension)
.option('-k, --keepext [file]', 'keep original file extension', c.keepext)
.option('-s, --source [path]', 'output code in a given folder', c.source)
.option('--cwd [path]', 'specify the Current Working Directory path for the purpose of generating qualified output filenames', c.cwd)
.option('-x, --separator [sep]', 'the source path is included in the output filename, separated by this separator (default: "-")', c.separator)
.option('-m, --marked-options [file]', 'use custom Marked options', c.marked_options)
.option('-i, --ignore [file]', 'ignore unsupported languages', c.ignore)
.option('-T, --tab-size [size]', 'convert leading tabs to X spaces')
.parse(args)
.name = "docco"
if commander.args.length
document commander
else
console.log commander.helpInformation()
Docco = module.exports = {run, document, parse, format, configure, version}