// **Gocco** is a Go 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 Gocco against its own source file.
//
// If you install Gocco, you can run it from the command-line:
//
// gocco *.go
//
// ...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 Gocco](http://github.com/nikhilm/gocco) is available on
// GitHub, and released under the MIT license.
//
// To install Gocco, first make sure you have [Pygments](http://pygments.org/)
// Then, with the go tool:
//
// go get github.com/nikhilm/gocco
package main
import (
"bytes"
"container/list"
"flag"
"github.com/russross/blackfriday"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strings"
"sync"
"text/template"
)
// ## Types
// Due to Go's statically typed nature, what is passed around in object
// literals in Docco, requires various structures
// A `Section` captures a piece of documentation and code
// Every time interleaving code is found between two comments
// a new `Section` is created.
type Section struct {
docsText []byte
codeText []byte
DocsHTML []byte
CodeHTML []byte
}
// a `TemplateSection` is a section that can be passed
// to Go's templating system, which expects strings.
type TemplateSection struct {
DocsHTML string
CodeHTML string
// The `Index` field is used to create anchors to sections
Index int
}
// a `Language` describes a programming language
type Language struct {
// the `Pygments` name of the language
name string
// The comment delimiter
symbol string
// The regular expression to match the comment delimiter
commentMatcher *regexp.Regexp
// Used as a placeholder so we can parse back Pygments output
// and put the sections together
dividerText string
// The HTML equivalent
dividerHTML *regexp.Regexp
}
// a `TemplateData` is per-file
type TemplateData struct {
// Title of the HTML output
Title string
// The Sections making up this file
Sections []*TemplateSection
// A full list of source files so that a table-of-contents can
// be generated
Sources []string
// Only generate the TOC is there is more than one file
// Go's templating system does not allow expressions in the
// template, so calculate it outside
Multiple bool
}
// a map of all the languages we know
var languages map[string]*Language
// paths of all the source files, sorted
var sources []string
// absolute path to get resources
var packageLocation string
// Wrap the code in these
const highlightStart = "
"
// ## Main documentation generation functions
// Generate the documentation for a single source file
// by splitting it into sections, highlighting each section
// and putting it together.
// The WaitGroup is used to signal we are done, so that the main
// goroutine waits for all the sub goroutines
func generateDocumentation(source string, wg *sync.WaitGroup) {
code, err := ioutil.ReadFile(source)
if err != nil {
log.Panic(err)
}
sections := parse(source, code)
highlight(source, sections)
generateHTML(source, sections)
wg.Done()
}
// Parse splits code into `Section`s
func parse(source string, code []byte) *list.List {
lines := bytes.Split(code, []byte("\n"))
sections := new(list.List)
sections.Init()
language := getLanguage(source)
var hasCode bool
var codeText = new(bytes.Buffer)
var docsText = new(bytes.Buffer)
// save a new section
save := func(docs, code []byte) {
// deep copy the slices since slices always refer to the same storage
// by default
docsCopy, codeCopy := make([]byte, len(docs)), make([]byte, len(code))
copy(docsCopy, docs)
copy(codeCopy, code)
sections.PushBack(&Section{docsCopy, codeCopy, nil, nil})
}
for _, line := range lines {
// if the line is a comment
if language.commentMatcher.Match(line) {
// but there was previous code
if hasCode {
// we need to save the existing documentation and text
// as a section and start a new section since code blocks
// have to be delimited before being sent to Pygments
save(docsText.Bytes(), codeText.Bytes())
hasCode = false
codeText.Reset()
docsText.Reset()
}
docsText.Write(language.commentMatcher.ReplaceAll(line, nil))
docsText.WriteString("\n")
} else {
hasCode = true
codeText.Write(line)
codeText.WriteString("\n")
}
}
// save any remaining parts of the source file
save(docsText.Bytes(), codeText.Bytes())
return sections
}
// `highlight` pipes the source to Pygments, section by section
// delimited by dividerText, then reads back the highlighted output,
// searches for the delimiters and extracts the HTML version of the code
// and documentation for each `Section`
func highlight(source string, sections *list.List) {
language := getLanguage(source)
pygments := exec.Command("pygmentize", "-l", language.name, "-f", "html", "-O", "encoding=utf-8")
pygmentsInput, _ := pygments.StdinPipe()
pygmentsOutput, _ := pygments.StdoutPipe()
// start the process before we start piping data to it
// otherwise the pipe may block
pygments.Start()
for e := sections.Front(); e != nil; e = e.Next() {
pygmentsInput.Write(e.Value.(*Section).codeText)
if e.Next() != nil {
io.WriteString(pygmentsInput, language.dividerText)
}
}
pygmentsInput.Close()
buf := new(bytes.Buffer)
io.Copy(buf, pygmentsOutput)
output := buf.Bytes()
output = bytes.Replace(output, []byte(highlightStart), nil, -1)
output = bytes.Replace(output, []byte(highlightEnd), nil, -1)
for e := sections.Front(); e != nil; e = e.Next() {
index := language.dividerHTML.FindIndex(output)
if index == nil {
index = []int{len(output), len(output)}
}
fragment := output[0:index[0]]
output = output[index[1]:]
e.Value.(*Section).CodeHTML = bytes.Join([][]byte{[]byte(highlightStart), []byte(highlightEnd)}, fragment)
e.Value.(*Section).DocsHTML = blackfriday.MarkdownCommon(e.Value.(*Section).docsText)
}
}
// compute the output location (in `docs/`) for the file
func destination(source string) string {
base := filepath.Base(source)
return "docs/" + base[0:strings.LastIndex(base, filepath.Ext(base))] + ".html"
}
// render the final HTML
func generateHTML(source string, sections *list.List) {
title := filepath.Base(source)
dest := destination(source)
// convert every `Section` into corresponding `TemplateSection`
sectionsArray := make([]*TemplateSection, sections.Len())
for e, i := sections.Front(), 0; e != nil; e, i = e.Next(), i+1 {
var sec = e.Value.(*Section)
docsBuf := bytes.NewBuffer(sec.DocsHTML)
codeBuf := bytes.NewBuffer(sec.CodeHTML)
sectionsArray[i] = &TemplateSection{docsBuf.String(), codeBuf.String(), i + 1}
}
// run through the Go template
html := goccoTemplate(TemplateData{title, sectionsArray, sources, len(sources) > 1})
log.Println("gocco: ", source, " -> ", dest)
ioutil.WriteFile(dest, html, 0644)
}
func goccoTemplate(data TemplateData) []byte {
// this hack is required because `ParseFiles` doesn't
// seem to work properly, always complaining about empty templates
t, err := template.New("gocco").Funcs(
// introduce the two functions that the template needs
template.FuncMap{
"base": filepath.Base,
"destination": destination,
}).Parse(HTML)
if err != nil {
panic(err)
}
buf := new(bytes.Buffer)
err = t.Execute(buf, data)
if err != nil {
panic(err)
}
return buf.Bytes()
}
// get a `Language` given a path
func getLanguage(source string) *Language {
return languages[filepath.Ext(source)]
}
// make sure `docs/` exists
func ensureDirectory(name string) {
os.MkdirAll(name, 0755)
}
func setupLanguages() {
languages = make(map[string]*Language)
// you should add more languages here
// only the first two fields should change, the rest should
// be `nil, "", nil`
languages[".go"] = &Language{"go", "//", nil, "", nil}
}
func setup() {
setupLanguages()
// create the regular expressions based on the language comment symbol
for _, lang := range languages {
lang.commentMatcher, _ = regexp.Compile("^\\s*" + lang.symbol + "\\s?")
lang.dividerText = "\n" + lang.symbol + "DIVIDER\n"
lang.dividerHTML, _ = regexp.Compile("\\n*" + lang.symbol + "DIVIDER<\\/span>\\n*")
}
}
// let's Go!
func main() {
setup()
flag.Parse()
sources = flag.Args()
sort.Strings(sources)
if flag.NArg() <= 0 {
return
}
ensureDirectory("docs")
ioutil.WriteFile("docs/gocco.css", bytes.NewBufferString(Css).Bytes(), 0755)
wg := new(sync.WaitGroup)
wg.Add(flag.NArg())
for _, arg := range flag.Args() {
go generateDocumentation(arg, wg)
}
wg.Wait()
}