Permalink
Newer
100644
894 lines (773 sloc)
28.4 KB
1
// # docker.js
2
// ### _A simple documentation generator based on [docco](http://jashkenas.github.com/docco/)_
3
// **Docker** is a really simple documentation generator, which originally started out as a
4
// pure-javascript port of **docco**, but which eventually gained many extra little features
5
// which somewhat break docco's philosophy of being a quick-and-dirty thing.
6
//
7
// Docker source-code can be found on [GitHub](https://github.com/jbt/docker)
8
//
9
// Take a look at the [original docco project](http://jashkenas.github.com/docco/) to get a feel
10
// for the sort of functionality this provides. In short: **Markdown**-based displaying of code comments
11
// next to syntax-highlighted code. This page is the result of running docker against itself.
12
//
13
// The command-line usage of docker is somewhat more useful than that of docco. To use, simply run
14
//
15
// ```sh
16
// ./docker -i path/to/code -o path/to/docs [a_file.js a_dir]
17
// ```
18
//
19
// Docker will then recurse into the code root directory (or alternatively just the files
20
// and directories you specify) and document-ize all the files it can.
21
// The folder structure will be preserved in the document root.
22
//
23
// More detailed usage instructions and examples can be found in the [README](../README.md)
24
//
25
// ## Differences from docco
26
// The main differences from docco are:
27
//
28
// - **jsDoc support**: support for **jsDoc**-style code comments, via [Dox](https://github.com/visionmedia/dox). You can see some examples of
29
// the sort of output you get below.
30
//
31
// - **Folder Tree** and **Heading Navigation**: collapsible sidebar with folder tree and jump-to
32
// heading links for easy navigation between many files and within long files.
33
//
34
// - **Markdown File Support**: support for plain markdown files, like the [README](../README.md) for this project.
35
//
36
// - **Colour Schemes**: support for multiple output colour schemes
37
//
38
//
39
// So let's get started!
40
41
// ## Node Modules
42
// Include lots of node modules
43
var stripIndent = require('strip-indent');
44
var MarkdownIt = require('markdown-it');
45
var highlight = require('highlight.js');
46
var repeating = require('repeating');
47
var mkdirp = require('mkdirp');
48
var extend = require('extend');
49
var watchr = require('watchr');
50
var async = require('async');
51
var path = require('path');
52
var less = require('less');
61
62
// Create an instance of markdown-it, which we'll use for prettyifying all the comments
67
if (lang && highlight.getLanguage(lang)) {
68
try {
69
return highlight.highlight(lang, str).value;
70
} catch (__) {}
71
}
78
// ## Markdown Link Overriding
79
//
80
// Relative links to files need to be remapped to their rendered file name,
81
// so that they can be written without `.html` everywhere else without breaking
82
md.renderer.rules.link_open = function(tokens, idx, options, env, self) {
83
var hrefIndex = tokens[idx].attrIndex('href');
84
85
// If the link a relative link, then put '.html' on the end.
86
if (hrefIndex >= 0 && !/\/\//.test(tokens[idx].attrs[hrefIndex][1])) {
87
tokens[idx].attrs[hrefIndex][1] += '.html';
88
}
89
90
return self.renderToken.apply(self, arguments);
91
};
92
93
94
/**
95
* ## Docker Constructor
96
*
97
* Creates a new docker instance. All methods are called on one instance of this object.
98
*
99
* Input is an `opts` containing all the options as specified below.
100
*/
104
inDir: path.resolve('.'),
105
outDir: path.resolve('doc'),
106
onlyUpdated: false,
107
colourScheme: 'default',
108
ignoreHidden: false,
109
sidebarState: true,
136
opts.js.push(path.join(extrasRoot, e, e + '.js'));
137
opts.css.push(path.join(extrasRoot, e, e + '.css'));
138
});
141
142
/**
143
* ## Docker.prototype.doc
144
*
145
* Generate documentation for a bunch of files
146
*
147
* @this Docker
148
* @param {Array} files Array of file paths relative to the `inDir` to generate documentation for.
149
*/
158
/**
159
* ## Docker.prototype.watch
160
*
161
* Watches the input directory for file changes and updates docs whenever a file is updated
162
*
163
* @param {Array} files Array of file paths relative to the `inDir` to generate documentation for.
164
*/
166
this.watching = true;
167
this.watchFiles = files;
168
169
// Function to call when a file is changed. We put this on a timeout to account
170
// for several file changes happening in quick succession.
171
var uto = false, self = this;
172
function update() {
173
if (self.running) return (uto = setTimeout(update, 250));
190
191
/**
192
* ## Docker.prototype.run
193
*
194
* Loops through all the queued file and processes them individually
195
*/
200
201
// While we stil have any files to process, take the first one and process it
210
// Once we're done, say we're no longer running and copy over all the static stuff
217
218
/**
219
* ## Docker.prototype.addFileToFree
220
*
221
* Adds a file to the file tree to show in the sidebar.
222
*
223
* @param {string} filename Name of file to add to the tree
224
*/
226
// Split the file's path into the individual directories
227
filename = filename.replace(new RegExp('^' + path.sep.replace(/([\/\\])/g, '\\$1')), '');
228
var bits = filename.split(path.sep);
229
230
// Loop through all the directories and process the folder structure into `this.tree`.
231
//
232
// `this.tree` takes the format:
233
// ```js
234
// {
235
// dirs: {
236
// 'child_dir_name': { /* same format as tree */ },
237
// 'other_child_name': // etc...
238
// },
239
// files: [
240
// 'filename.js',
241
// 'filename2.js',
242
// // etc...
243
// ]
244
// }
245
// ```
246
var currDir = this.tree;
247
var lastBit = bits.pop();
248
249
bits.forEach(function(bit) {
250
if (!currDir.dirs) currDir.dirs = {};
251
if (!currDir.dirs[bit]) currDir.dirs[bit] = {};
257
};
258
259
260
/**
261
* ## Docker.prototype.process
262
*
263
* Process the given file. If it's a directory, list all the children and queue those.
264
* If it's a file, add it to the queue.
265
*
266
* @param {string} file Path to the file to process
267
* @param {function} cb Callback to call when done
268
*/
270
// If we should be ignoring this file, do nothing and immediately callback.
281
// Something unexpected happened on the filesystem.
282
// Nothing really that we can do about it, so throw it and be done with it
283
return cb(err);
284
}
286
if (stat && stat.isSymbolicLink()) {
287
fs.readlink(resolved, function(err, link) {
288
if (err) {
289
// Something unexpected happened on the filesystem.
290
// Nothing really that we can do about it, so throw it and be done with it
291
return cb(err);
292
}
296
fs.exists(resolved, function(exists) {
297
if (!exists) {
298
console.error('Unable to follow symlink to ' + resolved + ': file does not exist');
309
// Something unexpected happened on the filesystem.
310
// Nothing really that we can do about it, so throw it and be done with it
311
return cb(err);
312
}
315
// For everything in the directory, queue it unless it looks hiden and we've
316
// been told to ignore hidden files.
329
330
/**
331
* ## Docker.prototype.processFile
332
*
333
* Processes a given file. At this point we know the file exists and
334
* isn't any kind of directory or symlink.
335
*
336
* @param {string} file Path to the file to process
337
* @param {function} cb Callback to call when done
338
*/
343
// First, check to see whether we actually should be processing this file and bail if not
344
this.decideWhetherToProcess(resolved, function(shouldProcess) {
345
if (!shouldProcess) return cb();
347
fs.readFile(resolved, 'utf-8', function(err, data) {
348
if (err) return cb(err);
350
// Grab the language details for the file and bail if we don't understand it.
356
switch (lang.type) {
357
case 'markdown':
358
self.renderMarkdownFile(data, resolved, cb);
359
break;
360
default:
361
case 'code':
362
var sections = self.parseSections(data, lang);
363
self.highlight(sections, lang);
364
self.renderCodeFile(sections, lang, resolved, cb);
365
break;
372
/**
373
* ## Docker.prototype.decideWhetherToProcess
374
*
375
* Decide whether or not a file should be processed. If the `onlyUpdated`
376
* flag was set on initialization, only allow processing of files that
377
* are newer than their counterpart generated doc file.
378
*
379
* Fires a callback function with either true or false depending on whether
380
* or not the file should be processed
381
*
382
* @param {string} filename The name of the file to check
383
* @param {function} callback Callback function
384
*/
386
// If we should be processing all files, then yes, we should process this one
388
389
// Find the doc this file would be compiled to
390
var outFile = this.outFile(filename);
391
392
// See whether the file is newer than the output
393
this.fileIsNewer(filename, outFile, callback);
394
};
395
397
/**
398
* ## Docker.prototype.fileIsNewer
399
*
400
* Sees whether one file is newer than another
401
*
402
* @param {string} file File to check
403
* @param {string} otherFile File to compare to
404
* @param {function} callback Callback to fire with true if file is newer than otherFile
405
*/
406
Docker.prototype.fileIsNewer = function(file, otherFile, callback) {
407
fs.stat(otherFile, function(err, outStat) {
412
// Process the file if the input is newer than the output
413
callback(+inStat.mtime > +outStat.mtime);
414
});
415
});
416
};
417
418
419
/**
420
* ## Docker.prototype.parseSections
421
*
422
* Parse the content of a file into individual sections.
423
* A section is defined to be one block of code with an accompanying comment
424
*
425
* Returns an array of section objects, which take the form
426
* ```js
427
* {
428
* doc_text: 'foo', // String containing comment content
429
* code_text: 'bar' // Accompanying code
430
* }
431
* ```
432
* @param {string} data The contents of the script file
433
* @param {object} lang The language data for the script file
434
* @return {Array} array of section objects
435
*/
450
var commentRegex = new RegExp('^\\s*' + lang.comment + '\\s?');
451
452
var self = this;
453
454
455
function mark(a, stripParas) {
456
var h = md.render(a.replace(/(^\s*|\s*$)/, ''));
457
return stripParas ? h.replace(/<\/?p>/g, '') : h;
462
var matchable = line.replace(/(["'])((?:[^\\\1]|(?:\\\\)*?\\[^\\])*?)\1/g, '$1$1');
463
if (lang.literals) {
464
lang.literals.forEach(function(replace) {
472
// End-multiline comments should match regardless of whether they're 'quoted'
478
multiLine += line;
479
480
// Replace block comment delimiters with whitespace of the same length
481
// This way we can safely outdent without breaking too many things if the
482
// comment has been deliberately indented. For example, the lines in the
483
// followinc comment should all be outdented equally:
484
//
485
// ```c
486
// /* A big long multiline
487
// comment that should get
488
// outdented properly */
489
// ```
490
multiLine = multiLine
491
.replace(lang.multiLine[0], function(a) { return repeating(' ', a.length); })
492
.replace(lang.multiLine[1], function(a) { return repeating(' ', a.length); });
501
502
// Put markdown parser on the data so it can be accessed in the template
503
jsDocData.md = mark;
504
section.docs += self.renderTemplate('jsDoc', jsDocData);
514
// We want to match the start of a multiline comment only if the line doesn't also match the
515
// end of the same comment, or if a single-line comment is started before the multiline
516
// So for example the following would not be treated as a multiline starter:
517
// ```js
522
(!lang.comment || !matchable.split(lang.multiLine[0])[0].match(commentRegex))
524
// Here we start parsing a multiline comment. Store away the current section and start a new one
525
if (section.code) {
526
if (!section.code.match(/^\s*$/) || !section.docs.match(/^\s*$/)) sections.push(section);
541
// This is for single-line comments. Again, store away the last section and start a new one
542
if (section.code) {
543
if (!section.code.match(/^\s*$/) || !section.docs.match(/^\s*$/)) sections.push(section);
544
section = { docs: '', code: '' };
545
}
546
section.docs += line.replace(commentRegex, '') + '\n';
548
// If this is the first line of active code, store it in the section
549
// so we can grab it for line numbers later
561
562
/**
563
* ## Docker.prototype.detectLanguage
564
*
565
* Provides language-specific params for a given file name.
566
*
567
* @param {string} filename The name of the file to test
568
* @param {string} contents The contents of the file (to check for shebang)
569
* @return {object} Object containing all of the language-specific params
570
*/
572
// First try to detect the language from the file extension
573
var ext = path.extname(filename);
574
ext = ext.replace(/^\./, '');
575
576
// Bit of a hacky way of incorporating .C for C++
578
ext = ext.toLowerCase();
579
580
var base = path.basename(filename);
581
base = base.toLowerCase();
582
583
for (var i in languages) {
584
if (!languages.hasOwnProperty(i)) continue;
585
if (languages[i].extensions &&
588
languages[i].names.indexOf(base) !== -1) return languages[i];
589
}
590
591
// If that doesn't work, see if we can grab a shebang
592
593
var shebangRegex = /^#!\s*(?:\/usr\/bin\/env)?\s*(?:[^\n]*\/)*([^\/\n]+)(?:\n|$)/;
595
if (match) {
596
for (var j in languages) {
597
if (!languages.hasOwnProperty(j)) continue;
598
if (languages[j].executables && languages[j].executables.indexOf(match[1]) !== -1) return languages[j];
599
}
600
}
601
602
// If we still can't figure it out, give up and return false.
603
return false;
604
};
605
606
607
/**
608
* ## Docker.prototype.highlight
609
*
610
* Highlights all the sections of a file using **highlightjs**
611
* Given an array of section objects, loop through them, and for each
612
* section generate pretty html for the comments and the code, and put them in
613
* `docHtml` and `codeHtml` respectively
614
*
615
* @param {Array} sections Array of section objects
616
* @param {string} language Language ith which to highlight the file
617
*/
618
Docker.prototype.highlight = function(sections, lang) {
619
sections.forEach(function(section) {
620
section.codeHtml = highlight.highlight(lang.highlightLanguage || lang.language, section.code).value;
625
626
/**
627
* ## Docker.prototype.addAnchors
628
*
629
* Automatically assign an id to each section based on any headings using **toc** helpers
630
*
631
* @param {object} section The section object to look at
632
* @param {number} idx The index of the section in the whole array.
633
* @param {Object} headings Object in which to keep track of headings for avoiding clashes
634
*/
636
var headingRegex = /<h(\d)(\s*[^>]*)>([\s\S]+?)<\/h\1>/gi; // toc.defaults.headers
637
639
// If there is a heading tag, pick out the first one (likely the most important), sanitize
640
// the name a bit to make it more friendly for IDs, then use that
642
var id = toc.unique(headings.ids, toc.anchor(content));
643
644
headings.list.push({ id: id, text: toc.untag(content), level: level });
645
return [
646
'<div class="pilwrap" id="' + id + '">',
647
' <h' + level + attrs + '>',
648
' <a href="#' + id + '" name="' + id + '" class="pilcrow"></a>',
655
// If however we can't find a heading, then just use the section index instead.
656
docHtml = [
657
'<div class="pilwrap">',
658
' <a class="pilcrow" href="#section-' + (idx + 1) + '" id="section-' + (idx + 1) + '"></a>',
659
'</div>',
660
docHtml
661
].join('\n');
662
}
663
664
return docHtml;
665
};
666
667
668
/**
669
* ## Docker.prototype.addLineNumbers
670
*
671
* Adds line numbers to rendered code HTML
672
*
673
* @param {string} html The code HTML
674
* @param {number} first Line number of the first code line
675
*/
680
var n = first + i;
681
return '<a class="line-num" href="#line-' + n + '" id="line-' + n + '" data-line="' + n + '"></a> ' + line;
682
});
683
684
return lines.join('\n');
685
};
686
687
688
/**
689
* ## Docker.prototype.renderCodeFile
690
*
691
* Given an array of sections, render them all out to a nice HTML file
692
*
693
* @param {Array} sections Array of sections containing parsed data
694
* @param {Object} language The language data for the file in question
695
* @param {string} filename Name of the file being processed
696
* @param {function} cb Callback function to fire when we're done
697
*/
698
Docker.prototype.renderCodeFile = function(sections, language, filename, cb) {
709
section.codeHtml = self.addLineNumbers(section.codeHtml, section.firstCodeLine);
713
var content = this.renderTemplate('code', {
714
title: path.basename(filename),
715
sections: sections,
716
language: language.language
717
});
722
723
/**
724
* ## Docker.prototype.renderMarkdownFile
725
*
726
* Renders the output for a Markdown file into HTML
727
*
728
* @param {string} data The markdown file content
729
* @param {string} filename Name of the file being processed
730
* @param {function} cb Callback function to fire when we're done
731
*/
733
var content = md.render(data);
734
735
var headings = { ids: {}, list: [] };
736
737
// Add anchors to all headings
739
740
// Wrap up with necessary classes
741
content = '<div class="docs markdown">' + content + '</div>';
742
743
this.makeOutputFile(filename, content, headings, cb);
744
};
745
746
747
/**
748
* ## Docker.prototype.makeOutputFile
749
*
750
* Shared code for generating an output file with the given content.
751
* Renders the given content in a template along with its headings and
752
* writes it to the output file.
753
*
754
* @param {string} filename Path to the input file
755
* @param {string} content The string content to render into the template
756
* @param {Object} headings List of headings + ids
757
* @param {function} cb Callback to call when done
758
*/
759
Docker.prototype.makeOutputFile = function(filename, content, headings, cb) {
760
// Decide which path to store the output on.
761
var outFile = this.outFile(filename);
762
763
// Calculate the location of the input root relative to the output file.
764
// This is necessary so we can link to the stylesheet in the output HTML using
765
// a relative href rather than an absolute one
766
var outDir = path.dirname(outFile);
767
var relativeOut = path.resolve(outDir)
770
var levels = relativeOut == '' ? 0 : relativeOut.split(path.sep).length;
771
var relDir = repeating('../', levels);
772
773
// Render the html file using our template
774
var html = this.renderTemplate('tmpl', {
775
title: path.basename(filename),
776
relativeDir: relDir,
777
content: content,
778
headings: headings,
779
sidebar: this.options.sidebarState,
780
filename: filename.replace(this.options.inDir, '').replace(/^[\\\/]/, ''),
781
js: this.options.js.map(function(f) { return path.basename(f); }),
782
css: this.options.css.map(function(f) { return path.basename(f); })
785
// Recursively create the output directory, clean out any old version of the
786
// output file, then save our new file.
787
this.writeFile(outFile, html, 'Generated: ' + outFile.replace(this.options.outDir, ''), cb);
788
};
791
/**
792
* ## Docker.prototype.copySharedResources
793
*
794
* Copies the shared CSS and JS files to the output directories
795
*/
798
self.writeFile(
799
path.join(self.options.outDir, 'doc-filelist.js'),
800
'var tree=' + JSON.stringify(self.tree) + ';',
801
'Saved file tree to doc-filelist.js'
802
);
805
fs.readFile(path.join(__dirname, '..', 'res', 'style.less'), function(err, file) {
806
// Now try to grab the colours out of whichever highlight theme was used
807
var hlpath = require.resolve('highlight.js');
808
var cspath = path.resolve(path.dirname(hlpath), '..', 'styles');
809
var colours = require('./getColourScheme')(self.options.colourScheme);
812
less.render(file.toString().replace('COLOURSCHEME', self.options.colourScheme), {
817
self.writeFile(
818
path.join(self.options.outDir, 'doc-style.css'),
819
out.css,
820
'Compiled CSS to doc-style.css'
821
);
822
});
823
});
825
fs.readFile(path.join(__dirname, '..', 'res', 'script.js'), function(err, file) {
826
self.writeFile(
827
path.join(self.options.outDir, 'doc-script.js'),
828
file,
829
'Copied JS to doc-script.js'
830
);
831
});
842
/**
843
* ## Docker.prototype.outFile
844
*
845
* Generates the output path for a given input file
846
*
847
* @param {string} filename Name of the input file
848
* @return {string} Name to use for the generated doc file
849
*/
851
return path.normalize(filename.replace(path.resolve(this.options.inDir), this.options.outDir) + '.html');
852
};
854
855
/**
856
* ## Docker.prototype.renderTemplate
857
*
858
* Renders an EJS template with the given data
859
*
860
* @param {string} templateName The name of the template to use
861
* @param {object} obj Object containing parameters for the template
862
* @return {string} Rendered output
863
*/
865
// If we haven't already loaded the template, load it now.
866
// It's a bit messy to be using readFileSync I know, but this
867
// is the easiest way for now.
868
if (!this._templates) this._templates = {};
869
if (!this._templates[templateName]) {
870
var tmplFile = path.join(__dirname, '..', 'res', templateName + '.ejs');
871
this._templates[templateName] = ejs.compile(fs.readFileSync(tmplFile).toString());
877
/**
878
* ## Docker.prototype.writeFile
879
*
880
* Saves a file, making sure the directory already exists and overwriting any existing file
881
*
882
* @param {string} filename The name of the file to save
883
* @param {string} fileContent Content to save to the file
884
* @param {string} doneLog String to console.log when done
885
* @param {function} doneCallback Callback to fire when done
886
*/
887
Docker.prototype.writeFile = function(filename, fileContent, doneLog, doneCallback) {
888
mkdirp(path.dirname(filename), function() {
889
fs.writeFile(filename, fileContent, function() {
890
if (doneLog) console.log(doneLog);
891
if (doneCallback) doneCallback();