-
Notifications
You must be signed in to change notification settings - Fork 572
/
docco.js
394 lines (351 loc) · 15.6 KB
/
docco.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
// Generated by CoffeeScript 2.6.1
(function() {
// Docco
// =====
// **Docco** is a quick-and-dirty documentation generator, written in
// [Literate CoffeeScript](http://coffeescript.org/#literate).
// It produces an HTML document that displays your comments intermingled with your
// code. All prose is passed through
// [Markdown](http://daringfireball.net/projects/markdown/syntax), and code is
// passed through [Highlight.js](http://highlightjs.org/) syntax highlighting.
// This page is the result of running Docco against its own
// [source file](https://github.com/jashkenas/docco/blob/master/docco.litcoffee).
// 1. Install Docco with **npm**: `sudo npm install -g docco`
// 2. 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](http://github.com/jashkenas/docco) is available on GitHub,
// and is released under the [Lil License](http://lillicense.org/v1.html).
// 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](https://github.com/jashkenas/docco/blob/master/resources/languages.json).
// Finally, the ["literate" style](http://coffeescript.org/#literate) of *any*
// language listed in [languages.json](https://github.com/jashkenas/docco/blob/master/resources/languages.json)
// is also supported — just tack an `.md` extension on the end:
// `.coffee.md`, `.py.md`, and so on.
// Partners in Crime:
// ------------------
// * If Node.js doesn't run on your platform, or you'd prefer a more
// convenient package, get [Ryan Tomayko](http://github.com/rtomayko)'s
// [Rocco](http://rtomayko.github.io/rocco/rocco.html), the **Ruby** port that's
// available as a gem.
// * If you're writing shell scripts, try
// [Shocco](http://rtomayko.github.io/shocco/), a port for the **POSIX shell**,
// also by Mr. Tomayko.
// * If **Python** is more your speed, take a look at
// [Nick Fitzgerald](http://github.com/fitzgen)'s [Pycco](https://pycco-docs.github.io/pycco/).
// * For **Clojure** fans, [Fogus](http://blog.fogus.me/)'s
// [Marginalia](http://fogus.me/fun/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](http://nikhilm.github.io/gocco/),
// written by [Nikhil Marathe](https://github.com/nikhilm).
// * For all you **PHP** buffs out there, Fredi Bach's
// [sourceMakeup](http://jquery-jkit.com/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](https://github.com/rgieseke)'s [Locco](http://rgieseke.github.io/locco/).
// * And if you happen to be a **.NET**
// aficionado, check out [Don Wilson](https://github.com/dontangg)'s
// [Nocco](http://dontangg.github.io/nocco/).
// * Going further afield from the quick-and-dirty, [Groc](http://nevir.github.io/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.
// * For **ES6** fans, [Docco Next](https://github.com/mobily-enterprises/docco-next)
// is an expanded rewrite of Docco in modern JavaScript, thoroughly commented,
// and with the bonus that your existing templates will still work.
// Note that not all ports support all Docco features.
// Main Documentation Generation Functions
// ---------------------------------------
// 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, and printing them
// out in an HTML template.
var Docco, _, buildMatchers, commander, configure, defaults, document, format, fs, getLanguage, highlightjs, languages, marked, parse, path, run, version, write;
document = function(options = {}, callback) {
var config;
config = configure(options);
return fs.mkdirs(config.output, function() {
var complete, copyAsset, files, nextFile;
callback || (callback = function(error) {
if (error) {
throw error;
}
});
copyAsset = function(file, callback) {
if (!fs.existsSync(file)) {
return callback();
}
return fs.copy(file, path.join(config.output, path.basename(file)), callback);
};
complete = function() {
return copyAsset(config.css, function(error) {
if (error) {
return callback(error);
}
if (fs.existsSync(config.public)) {
return copyAsset(config.public, callback);
}
return callback();
});
};
files = config.sources.slice();
nextFile = function() {
var source;
source = files.shift();
return fs.readFile(source, function(error, buffer) {
var code, sections;
if (error) {
return callback(error);
}
code = buffer.toString();
sections = parse(source, code, config);
format(source, sections, config);
write(source, sections, config);
if (files.length) {
return nextFile();
} else {
return complete();
}
});
};
return nextFile();
});
};
// 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 = function(source, code, config = {}) {
var codeText, docsText, hasCode, i, isText, j, k, lang, len, len1, line, lines, match, maybeCode, save, sections;
lines = code.split('\n');
sections = [];
lang = getLanguage(source, config);
hasCode = docsText = codeText = '';
save = function() {
sections.push({docsText, codeText});
return 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.
if (lang.literate) {
isText = maybeCode = true;
for (i = j = 0, len = lines.length; j < len; i = ++j) {
line = lines[i];
lines[i] = maybeCode && (match = /^([ ]{4}|[ ]{0,3}\t)/.exec(line)) ? (isText = false, line.slice(match[0].length)) : (maybeCode = /^\s*$/.test(line)) ? isText ? lang.symbol : '' : (isText = true, lang.symbol + ' ' + line);
}
}
for (k = 0, len1 = lines.length; k < len1; k++) {
line = lines[k];
if (line.match(lang.commentMatcher) && !line.match(lang.commentFilter)) {
if (hasCode) {
save();
}
docsText += (line = line.replace(lang.commentMatcher, '')) + '\n';
if (/^(---+|===+)$/.test(line)) {
save();
}
} else {
hasCode = true;
codeText += line + '\n';
}
}
save();
return 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](https://github.com/chjj/marked).
format = function(source, sections, config) {
var code, i, j, language, len, markedOptions, results, section;
language = getLanguage(source, config);
// Pass any user defined options to Marked if specified via command line option
markedOptions = {
smartypants: true
};
if (config.marked) {
markedOptions = config.marked;
}
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: function(code, lang) {
lang || (lang = language.name);
if (highlightjs.getLanguage(lang)) {
return highlightjs.highlight(code, {
language: lang
}).value;
} else {
console.warn(`docco: couldn't highlight code block with unknown language '${lang}' in ${source}`);
return code;
}
}
});
results = [];
for (i = j = 0, len = sections.length; j < len; i = ++j) {
section = sections[i];
code = highlightjs.highlight(section.codeText, {
language: language.name
}).value;
code = code.replace(/\s+$/, '');
section.codeHtml = `<div class='highlight'><pre>${code}</pre></div>`;
results.push(section.docsHtml = marked(section.docsText));
}
return results;
};
// 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 = function(source, sections, config) {
var css, destination, first, firstSection, hasTitle, html, relative, title;
destination = function(file) {
return path.join(config.output, path.dirname(file), path.basename(file, path.extname(file)) + '.html');
};
relative = function(file) {
var from, to;
to = path.dirname(path.resolve(file));
from = path.dirname(path.resolve(destination(source)));
return path.join(path.relative(from, to), path.basename(file));
};
// The **title** of the file is either the first heading in the prose, or the
// name of the source file.
firstSection = _.find(sections, function(section) {
return section.docsText.length > 0;
});
if (firstSection) {
first = marked.lexer(firstSection.docsText)[0];
}
hasTitle = first && first.type === 'heading' && first.depth === 1;
title = hasTitle ? first.text : path.basename(source);
css = relative(path.join(config.output, path.basename(config.css)));
html = config.template({
sources: config.sources,
css,
title,
hasTitle,
sections,
path,
destination,
relative
});
console.log(`docco: ${source} -> ${destination(source)}`);
return fs.outputFileSync(destination(source), html);
};
// Configuration
// -------------
// Default configuration **options**. All of these may be extended by
// user-specified options.
defaults = {
layout: 'parallel',
output: 'docs',
template: null,
css: null,
extension: null,
languages: {},
marked: null
};
// **Configure** this particular run of Docco. We might use a passed-in external
// template, or one of the built-in **layouts**. We only attempt to process
// source files for languages for which we have definitions.
configure = function(options) {
var config, dir;
config = _.extend({}, defaults, _.pick(options.opts(), ..._.keys(defaults)));
config.languages = buildMatchers(config.languages);
// The user is able to override the layout file used with the `--template` parameter.
// In this case, it is also neccessary 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) {
if (!options.css) {
console.warn("docco: no stylesheet file specified");
}
config.layout = null;
} else {
dir = config.layout = path.join(__dirname, 'resources', config.layout);
if (fs.existsSync(path.join(dir, 'public'))) {
config.public = path.join(dir, 'public');
}
config.template = path.join(dir, 'docco.jst');
config.css = options.css || path.join(dir, 'docco.css');
}
config.template = _.template(fs.readFileSync(config.template).toString());
if (options.marked) {
config.marked = JSON.parse(fs.readFileSync(options.marked));
}
config.sources = options.args.filter(function(source) {
var lang;
lang = getLanguage(source, config);
if (!lang) {
console.warn(`docco: skipped unknown type (${path.basename(source)})`);
}
return lang;
}).sort();
return config;
};
// Helpers & Initial Setup
// -----------------------
// Require our external dependencies.
_ = require('underscore');
fs = require('fs-extra');
path = require('path');
marked = require('marked').marked;
commander = require('commander');
highlightjs = require('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 = function(languages) {
var ext, l;
for (ext in languages) {
l = languages[ext];
// Does the line begin with a comment?
l.commentMatcher = RegExp(`^\\s*${l.symbol}\\s?`);
// Ignore [hashbangs](http://en.wikipedia.org/wiki/Shebang_%28Unix%29) and interpolations...
l.commentFilter = /(^#![\/]|^\s*#\{)/;
}
return 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 = function(source, config) {
var codeExt, codeLang, ext, lang, ref, ref1;
ext = config.extension || path.extname(source) || path.basename(source);
lang = ((ref = config.languages) != null ? ref[ext] : void 0) || languages[ext];
if (lang && lang.name === 'markdown') {
codeExt = path.extname(path.basename(source, ext));
codeLang = ((ref1 = config.languages) != null ? ref1[codeExt] : void 0) || languages[codeExt];
if (codeExt && codeLang) {
lang = _.extend({}, codeLang, {
literate: true
});
}
}
return lang;
};
// Keep it DRY. Extract the docco **version** from `package.json`
version = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'))).version;
// Command Line Interface
// ----------------------
// Finally, let's define the interface to run Docco from the command line.
// Parse options using [Commander](https://github.com/visionmedia/commander.js).
run = function(args = process.argv) {
var c;
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 or classic)', 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('-e, --extension [ext]', 'assume a file extension for all inputs', c.extension).option('-m, --marked [file]', 'use custom marked options', c.marked).parse(args).name = "docco";
if (commander.args.length) {
return document(commander);
} else {
return console.log(commander.helpInformation());
}
};
// Public API
// ----------
Docco = module.exports = {run, document, parse, format, version};
}).call(this);