Locco is a Lua port of Docco, the 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 syntax highlighted. This page is the result of running Locco against its own source file:

locco.lua locco.lua

For its syntax highlighting Locco relies on the help of David Manura's Lua Balanced to split up the code. As a markdown engine it ships with Niklas Frykholm's markdown.lua in the Lua 5.2 compatible version from Patrick Gundlach. Otherwise there are no external dependencies.

The generated HTML documentation for the given source files is saved into a docs directory. If you have Locco on your path you can run it from the command-line:

locco.lua project/*.lua

Locco is monolingual, but there are many projects written in and with support for other languages, see the Docco page for a list.
The source for Locco is available on GitHub, and released under the MIT license.

Setup & Helpers

Add script path to package path to find submodules.

local script_path = arg[0]:match('(.+)/.+')
package.path = table.concat({
}, ';')

Load markdown.lua.

local md = require 'markdown'

Load Lua Balanced.

local lb = require 'luabalanced'

Load HTML templates.

local template = require 'template'

Ensure the docs directory exists and return the path of the source file.
source: The source file for which documentation is generated.

local function ensure_directory(source)
  local path = source:match('(.+)/.+$')
  if not path then path = '.' end
  os.execute('mkdir -p '..path..'/docs')
  return path

Insert HTML entities in a string.
s: String to escape.

local function escape(s)
  s = s:gsub('&', '&')
  s = s:gsub('<', '&lt;')
  s = s:gsub('>', '&gt;')
  s = s:gsub('%%', '&#37;')
  return s

local function replace_percent(s) s = s:gsub('%', '%%') return s end

Define the Lua keywords, built-in functions and operators that should be highlighted.

local keywords = { 'break', 'do', 'else', 'elseif', 'end', 'false', 'for',
                   'function', 'if', 'in', 'local', 'nil', 'repeat', 'return',
                   'then', 'true', 'until', 'while' }
local functions = { 'assert', 'collectgarbage', 'dofile', 'error', 'getfenv',
                    'getmetatable', 'ipairs', 'load', 'loadfile', 'loadstring',
                    'module', 'next', 'pairs', 'pcall', 'print', 'rawequal',
                    'rawget', 'rawset', 'require', 'setfenv', 'setmetatable',
                    'tonumber', 'tostring', 'type', 'unpack', 'xpcall' }
local operators = { 'and', 'not', 'or' }

Wrap an item from a list of Lua keywords in a span template or return the unchanged item.
item: An item of a code snippet.
item_list: List of keywords or functions.
span_class: Style sheet class.

local function wrap_in_span(item, item_list, span_class)
  for i=1, #item_list do
    if item_list[i] == item then
      item = '<span class="'..span_class..'">'..item..'</span>'
  return item

Quick and dirty source code highlighting. A chunk of code is split into comments (at the end of a line), strings and code using the Lua Balanced module. The code is then split again and matched against lists of Lua keywords, functions or operators. All Lua items are wrapped into a span having one of the classes defined in the Locco style sheet.
code: Chunk of code to highlight.

local function highlight_lua(code)
  local out = lb.gsub(code,
    function(u, s)
      local sout
      if u == 'c' then -- Comments.
        sout = '<span class="c">'..escape(s)..'</span>'
      elseif u == 's' then -- Strings.
        sout = '<span class="s">'..escape(s)..'</span>'
      elseif u == 'e' then -- Code.
        s = escape(s)

First highlight function names.

        s = s:gsub('function ([%w_:%.]+)', 'function <span class="nf">%1</span>')

There might be a non-keyword at the beginning of the snippet.

        sout = s:match('^(%A+)') or ''

Iterate through Lua items and try to wrap operators, keywords and built-in functions in span elements. If nothing was highlighted go to the next category.

        for item, sep in s:gmatch('([%a_]+)(%A+)') do
          local span, n = wrap_in_span(item, operators, 'o')
          if span == item then
            span, n = wrap_in_span(item, keywords, 'k')
          if span == item then
            span, n = wrap_in_span(item, functions, 'nt')
          sout = sout..span..sep
      return sout
    out = '<div class="highlight"><pre>'..out..'</pre></div>'
  return out

Main Documentation Generation Functions

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 = ...,

source: The source file to process.

local function parse(source)
  local sections = {}
  local has_code = false
  local docs_text, code_text = '', ''
  for line in io.lines(source) do
    if line:match('^%s*%-%-') then
      if has_code then
        code_text = code_text:gsub('\n\n$', '\n') -- remove empty trailing line
        sections[#sections + 1] = { ['docs_text'] = docs_text,
                                    ['code_text'] = code_text }
        has_code = false
        docs_text, code_text = '', ''
      docs_text = docs_text..line:gsub('%s*(%-%-%s?)', '', 1)..'\n'
      if not line:match('^#!') then -- ignore #!/usr/bin/lua
        has_code = true
        code_text = code_text..line..'\n'
  sections[#sections + 1] = { ['docs_text'] = docs_text,
                              ['code_text'] = code_text }
  return sections

Loop through a table of split sections and convert the documentation from Markdown to HTML and pass the code through Locco's syntax highlighting. Add docs_html and code_html elements to the sections table.
sections: A table with split sections.

local function highlight(sections)
  for i=1, #sections do
    sections[i]['docs_html'] = md.markdown(sections[i]['docs_text'])
    sections[i]['code_html'] = highlight_lua(sections[i]['code_text'])
  return sections

After the highlighting is done, the template is filled with the documentation and code snippets and an HTML file is written.
source: The source file.
path: Path of the source file.
filename: The filename of the source file.
sections: A table with the original sections and rendered as HTML.
jump_to: A HTML chunk with links to other documentation files.

local function generate_html(source, path, filename, sections, jump_to)
  local f, err = io.open(path..'/'..'docs/'..filename:gsub('lua$', 'html'), 'wb')
  if err then print(err) end
  local h = template.header:gsub('%%title%%', source)
  h = h:gsub('%%jump%%', jump_to)
  for i=1, #sections do
    local t = template.table_entry:gsub('%%index%%', i..'')
    t = t:gsub('%%docs_html%%', sections[i]['docs_html'])
    t = t:gsub('%%code_html%%', sections[i]['code_html'])

Generate the documentation for a source file by reading it in, splitting it up into comment/code sections, highlighting and merging them into an HTML template.
source: The source file to process.
path: Path of the source file.
filename: The filename of the source file.
jump_to: A HTML chunk with links to other documentation files.

local function generate_documentation(source, path, filename, jump_to)
  local sections = parse(source)
  local sections = highlight(sections)
  generate_html(source, path, filename, sections, jump_to)

Run the script.

Generate HTML links to other files in the documentation.

local jump_to = ''
if #arg > 1 then
  jump_to = template.jump_start
  for i=1, #arg do
    local link = arg[i]:gsub('lua$', 'html')
    link = link:match('.+/(.+)$') or link
    local t = template.jump:gsub('%%jump_html%%', link)
    t = t:gsub('%%jump_lua%%', arg[i])
    jump_to = jump_to..t
  jump_to = jump_to..template.jump_end

Make sure the output directory exists, generate the HTML files for each source file, print what's happening and write the style sheet.

local path = ensure_directory(arg[1])
for i=1, #arg do
  local filename = arg[i]:match('.+/(.+)$') or arg[i]
  generate_documentation(arg[i], path, filename, jump_to)
  print(arg[i]..' --> '..path..'/docs/'..filename:gsub('lua$', 'html'))
local f, err = io.open(path..'/'..'docs/locco.css', 'wb')
if err then print(err) end