247 |
248 |
249 | `
250 |
--------------------------------------------------------------------------------
/gocco.go:
--------------------------------------------------------------------------------
1 | // **Gocco** is a Go port of [Docco](http://jashkenas.github.com/docco/): the
2 | // original quick-and-dirty, hundred-line-long, literate-programming-style
3 | // documentation generator. It produces HTML that displays your comments
4 | // alongside your code. Comments are passed through
5 | // [Markdown](http://daringfireball.net/projects/markdown/syntax), and code is
6 | // passed through [Pygments](http://pygments.org/) syntax highlighting. This
7 | // page is the result of running Gocco against its own source file.
8 | //
9 | // If you install Gocco, you can run it from the command-line:
10 | //
11 | // gocco *.go
12 | //
13 | // ...will generate an HTML documentation page for each of the named source
14 | // files, with a menu linking to the other pages, saving it into a `docs`
15 | // folder.
16 | //
17 | // The [source for Gocco](http://github.com/nikhilm/gocco) is available on
18 | // GitHub, and released under the MIT license.
19 | //
20 | // To install Gocco, first make sure you have [Pygments](http://pygments.org/)
21 | // Then, with the go tool:
22 | //
23 | // go get github.com/nikhilm/gocco
24 | package main
25 |
26 | import (
27 | "bytes"
28 | "container/list"
29 | "flag"
30 | "github.com/russross/blackfriday"
31 | "io"
32 | "io/ioutil"
33 | "log"
34 | "os"
35 | "os/exec"
36 | "path/filepath"
37 | "regexp"
38 | "sort"
39 | "strings"
40 | "sync"
41 | "text/template"
42 | )
43 |
44 | // ## Types
45 | // Due to Go's statically typed nature, what is passed around in object
46 | // literals in Docco, requires various structures
47 |
48 | // A `Section` captures a piece of documentation and code
49 | // Every time interleaving code is found between two comments
50 | // a new `Section` is created.
51 | type Section struct {
52 | docsText []byte
53 | codeText []byte
54 | DocsHTML []byte
55 | CodeHTML []byte
56 | }
57 |
58 | // a `TemplateSection` is a section that can be passed
59 | // to Go's templating system, which expects strings.
60 | type TemplateSection struct {
61 | DocsHTML string
62 | CodeHTML string
63 | // The `Index` field is used to create anchors to sections
64 | Index int
65 | }
66 |
67 | // a `Language` describes a programming language
68 | type Language struct {
69 | // the `Pygments` name of the language
70 | name string
71 | // The comment delimiter
72 | symbol string
73 | // The regular expression to match the comment delimiter
74 | commentMatcher *regexp.Regexp
75 | // Used as a placeholder so we can parse back Pygments output
76 | // and put the sections together
77 | dividerText string
78 | // The HTML equivalent
79 | dividerHTML *regexp.Regexp
80 | }
81 |
82 | // a `TemplateData` is per-file
83 | type TemplateData struct {
84 | // Title of the HTML output
85 | Title string
86 | // The Sections making up this file
87 | Sections []*TemplateSection
88 | // A full list of source files so that a table-of-contents can
89 | // be generated
90 | Sources []string
91 | // Only generate the TOC is there is more than one file
92 | // Go's templating system does not allow expressions in the
93 | // template, so calculate it outside
94 | Multiple bool
95 | }
96 |
97 | // a map of all the languages we know
98 | var languages map[string]*Language
99 |
100 | // paths of all the source files, sorted
101 | var sources []string
102 |
103 | // absolute path to get resources
104 | var packageLocation string
105 |
106 | // Wrap the code in these
107 | const highlightStart = "
"
108 | const highlightEnd = "
"
109 |
110 | // ## Main documentation generation functions
111 |
112 | // Generate the documentation for a single source file
113 | // by splitting it into sections, highlighting each section
114 | // and putting it together.
115 | // The WaitGroup is used to signal we are done, so that the main
116 | // goroutine waits for all the sub goroutines
117 | func generateDocumentation(source string, wg *sync.WaitGroup) {
118 | code, err := ioutil.ReadFile(source)
119 | if err != nil {
120 | log.Panic(err)
121 | }
122 | sections := parse(source, code)
123 | highlight(source, sections)
124 | generateHTML(source, sections)
125 | wg.Done()
126 | }
127 |
128 | // Parse splits code into `Section`s
129 | func parse(source string, code []byte) *list.List {
130 | lines := bytes.Split(code, []byte("\n"))
131 | sections := new(list.List)
132 | sections.Init()
133 | language := getLanguage(source)
134 |
135 | var hasCode bool
136 | var codeText = new(bytes.Buffer)
137 | var docsText = new(bytes.Buffer)
138 |
139 | // save a new section
140 | save := func(docs, code []byte) {
141 | // deep copy the slices since slices always refer to the same storage
142 | // by default
143 | docsCopy, codeCopy := make([]byte, len(docs)), make([]byte, len(code))
144 | copy(docsCopy, docs)
145 | copy(codeCopy, code)
146 | sections.PushBack(&Section{docsCopy, codeCopy, nil, nil})
147 | }
148 |
149 | for _, line := range lines {
150 | // if the line is a comment
151 | if language.commentMatcher.Match(line) {
152 | // but there was previous code
153 | if hasCode {
154 | // we need to save the existing documentation and text
155 | // as a section and start a new section since code blocks
156 | // have to be delimited before being sent to Pygments
157 | save(docsText.Bytes(), codeText.Bytes())
158 | hasCode = false
159 | codeText.Reset()
160 | docsText.Reset()
161 | }
162 | docsText.Write(language.commentMatcher.ReplaceAll(line, nil))
163 | docsText.WriteString("\n")
164 | } else {
165 | hasCode = true
166 | codeText.Write(line)
167 | codeText.WriteString("\n")
168 | }
169 | }
170 | // save any remaining parts of the source file
171 | save(docsText.Bytes(), codeText.Bytes())
172 | return sections
173 | }
174 |
175 | // `highlight` pipes the source to Pygments, section by section
176 | // delimited by dividerText, then reads back the highlighted output,
177 | // searches for the delimiters and extracts the HTML version of the code
178 | // and documentation for each `Section`
179 | func highlight(source string, sections *list.List) {
180 | language := getLanguage(source)
181 | pygments := exec.Command("pygmentize", "-l", language.name, "-f", "html", "-O", "encoding=utf-8")
182 | pygmentsInput, _ := pygments.StdinPipe()
183 | pygmentsOutput, _ := pygments.StdoutPipe()
184 | // start the process before we start piping data to it
185 | // otherwise the pipe may block
186 | pygments.Start()
187 | for e := sections.Front(); e != nil; e = e.Next() {
188 | pygmentsInput.Write(e.Value.(*Section).codeText)
189 | if e.Next() != nil {
190 | io.WriteString(pygmentsInput, language.dividerText)
191 | }
192 | }
193 | pygmentsInput.Close()
194 |
195 | buf := new(bytes.Buffer)
196 | io.Copy(buf, pygmentsOutput)
197 |
198 | output := buf.Bytes()
199 | output = bytes.Replace(output, []byte(highlightStart), nil, -1)
200 | output = bytes.Replace(output, []byte(highlightEnd), nil, -1)
201 |
202 | for e := sections.Front(); e != nil; e = e.Next() {
203 | index := language.dividerHTML.FindIndex(output)
204 | if index == nil {
205 | index = []int{len(output), len(output)}
206 | }
207 |
208 | fragment := output[0:index[0]]
209 | output = output[index[1]:]
210 | e.Value.(*Section).CodeHTML = bytes.Join([][]byte{[]byte(highlightStart), []byte(highlightEnd)}, fragment)
211 | e.Value.(*Section).DocsHTML = blackfriday.MarkdownCommon(e.Value.(*Section).docsText)
212 | }
213 | }
214 |
215 | // compute the output location (in `docs/`) for the file
216 | func destination(source string) string {
217 | base := filepath.Base(source)
218 | return "docs/" + base[0:strings.LastIndex(base, filepath.Ext(base))] + ".html"
219 | }
220 |
221 | // render the final HTML
222 | func generateHTML(source string, sections *list.List) {
223 | title := filepath.Base(source)
224 | dest := destination(source)
225 | // convert every `Section` into corresponding `TemplateSection`
226 | sectionsArray := make([]*TemplateSection, sections.Len())
227 | for e, i := sections.Front(), 0; e != nil; e, i = e.Next(), i+1 {
228 | var sec = e.Value.(*Section)
229 | docsBuf := bytes.NewBuffer(sec.DocsHTML)
230 | codeBuf := bytes.NewBuffer(sec.CodeHTML)
231 | sectionsArray[i] = &TemplateSection{docsBuf.String(), codeBuf.String(), i + 1}
232 | }
233 | // run through the Go template
234 | html := goccoTemplate(TemplateData{title, sectionsArray, sources, len(sources) > 1})
235 | log.Println("gocco: ", source, " -> ", dest)
236 | ioutil.WriteFile(dest, html, 0644)
237 | }
238 |
239 | func goccoTemplate(data TemplateData) []byte {
240 | // this hack is required because `ParseFiles` doesn't
241 | // seem to work properly, always complaining about empty templates
242 | t, err := template.New("gocco").Funcs(
243 | // introduce the two functions that the template needs
244 | template.FuncMap{
245 | "base": filepath.Base,
246 | "destination": destination,
247 | }).Parse(HTML)
248 | if err != nil {
249 | panic(err)
250 | }
251 | buf := new(bytes.Buffer)
252 | err = t.Execute(buf, data)
253 | if err != nil {
254 | panic(err)
255 | }
256 | return buf.Bytes()
257 | }
258 |
259 | // get a `Language` given a path
260 | func getLanguage(source string) *Language {
261 | return languages[filepath.Ext(source)]
262 | }
263 |
264 | // make sure `docs/` exists
265 | func ensureDirectory(name string) {
266 | os.MkdirAll(name, 0755)
267 | }
268 |
269 | func setupLanguages() {
270 | languages = make(map[string]*Language)
271 | // you should add more languages here
272 | // only the first two fields should change, the rest should
273 | // be `nil, "", nil`
274 | languages[".go"] = &Language{"go", "//", nil, "", nil}
275 | }
276 |
277 | func setup() {
278 | setupLanguages()
279 |
280 | // create the regular expressions based on the language comment symbol
281 | for _, lang := range languages {
282 | lang.commentMatcher, _ = regexp.Compile("^\\s*" + lang.symbol + "\\s?")
283 | lang.dividerText = "\n" + lang.symbol + "DIVIDER\n"
284 | lang.dividerHTML, _ = regexp.Compile("\\n*" + lang.symbol + "DIVIDER<\\/span>\\n*")
285 | }
286 | }
287 |
288 | // let's Go!
289 | func main() {
290 | setup()
291 |
292 | flag.Parse()
293 | sources = flag.Args()
294 | sort.Strings(sources)
295 |
296 | if flag.NArg() <= 0 {
297 | return
298 | }
299 |
300 | ensureDirectory("docs")
301 | ioutil.WriteFile("docs/gocco.css", bytes.NewBufferString(Css).Bytes(), 0755)
302 |
303 | wg := new(sync.WaitGroup)
304 | wg.Add(flag.NArg())
305 | for _, arg := range flag.Args() {
306 | go generateDocumentation(arg, wg)
307 | }
308 | wg.Wait()
309 | }
310 |
--------------------------------------------------------------------------------