Skip to content
Permalink
Newer
Older
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
//
February 14, 2016 16:32
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
//
February 14, 2016 16:32
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
August 3, 2015 21:36
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');
53
var dox = require('dox');
August 3, 2015 21:36
54
var ejs = require('ejs');
55
var toc = require('toc');
August 3, 2015 21:36
56
var fs = require('fs');
57
58
// Language details exist in [languages.js](./languages.js)
August 3, 2015 21:36
59
var languages = require('./languages');
60
61
62
// Create an instance of markdown-it, which we'll use for prettyifying all the comments
August 3, 2015 21:36
63
var md = new MarkdownIt({
64
html: true,
65
langPrefix: '',
66
highlight: function(str, lang) {
August 3, 2015 21:36
67
if (lang && highlight.getLanguage(lang)) {
68
try {
69
return highlight.highlight(lang, str).value;
70
} catch (__) {}
71
}
August 3, 2015 21:36
75
});
February 14, 2016 16:32
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
*/
101
var Docker = module.exports = function(opts) {
102
// Initialise all opts with default values
August 3, 2015 21:36
103
opts = this.options = extend({
104
inDir: path.resolve('.'),
105
outDir: path.resolve('doc'),
106
onlyUpdated: false,
107
colourScheme: 'default',
108
ignoreHidden: false,
109
sidebarState: true,
October 7, 2012 10:31
110
exclude: false,
October 9, 2012 21:32
111
lineNums: false,
April 23, 2014 17:42
112
multiLineOnly: false,
October 7, 2012 10:31
113
js: [],
114
css: [],
115
extras: []
August 3, 2015 21:36
116
}, opts);
118
// Generate an exclude regex for the given pattern
119
if (typeof opts.exclude === 'string') {
120
this.excludePattern = new RegExp('^(' +
August 3, 2015 21:36
121
opts.exclude.replace(/\./g, '\\.')
122
.replace(/\*/g, '.*')
123
.replace(/,/g, '|') +
124
')(/|$)');
126
this.excludePattern = false;
127
}
128
129
// Initialise an object which'll store all our directory structure
August 3, 2015 21:36
130
this.tree = {};
132
// Load bundled extras
133
var extrasRoot = path.resolve(__dirname, '..', 'extras');
134
135
opts.extras.forEach(function(e) {
December 13, 2015 15:38
136
opts.js.push(path.join(extrasRoot, e, e + '.js'));
137
opts.css.push(path.join(extrasRoot, e, e + '.css'));
138
});
139
};
140
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
*/
150
Docker.prototype.doc = function(files) {
August 3, 2015 21:36
151
this.files = files.concat();
152
153
// Start processing, unless we already are
154
if (!this.running) this.run();
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
*/
165
Docker.prototype.watch = function(files) {
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));
174
self.doc(self.watchFiles);
175
uto = false;
176
}
177
178
// Create a watchr instance to watch all changes in the input directory
January 27, 2013 14:45
179
watchr.watch({
August 3, 2015 21:36
180
path: this.options.inDir,
181
listener: function() {
182
if (!uto) uto = setTimeout(update, 250);
January 27, 2013 14:45
183
}
184
});
185
186
// Aaaaand, go!
187
this.doc(files);
188
};
189
190
191
/**
192
* ## Docker.prototype.run
193
*
194
* Loops through all the queued file and processes them individually
195
*/
196
Docker.prototype.run = function() {
August 3, 2015 21:36
197
var self = this;
August 3, 2015 21:36
199
this.running = true;
200
201
// While we stil have any files to process, take the first one and process it
August 3, 2015 21:36
202
async.whilst(
204
return self.files.length > 0;
205
},
August 3, 2015 21:36
207
self.process(self.files.shift(), cb);
208
},
210
// Once we're done, say we're no longer running and copy over all the static stuff
August 3, 2015 21:36
211
self.running = false;
212
self.copySharedResources();
213
}
214
);
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
*/
225
Docker.prototype.addFileToTree = function(filename) {
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] = {};
252
currDir = currDir.dirs[bit];
253
});
254
if (!currDir.files) currDir.files = [];
256
if (currDir.files.indexOf(lastBit) === -1) currDir.files.push(lastBit);
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
*/
269
Docker.prototype.process = function(file, cb) {
270
// If we should be ignoring this file, do nothing and immediately callback.
271
if (this.excludePattern && this.excludePattern.test(file)) {
August 3, 2015 21:36
272
cb();
273
return;
August 3, 2015 21:36
276
var self = this;
August 3, 2015 21:36
278
var resolved = path.resolve(this.options.inDir, file);
279
fs.lstat(resolved, function lstatCb(err, stat) {
280
if (err) {
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
}
August 3, 2015 21:36
294
resolved = path.resolve(path.dirname(resolved), link);
October 3, 2012 21:47
295
296
fs.exists(resolved, function(exists) {
297
if (!exists) {
298
console.error('Unable to follow symlink to ' + resolved + ': file does not exist');
August 3, 2015 21:36
299
cb(null);
August 3, 2015 21:36
301
fs.lstat(resolved, lstatCb);
302
}
303
});
304
});
305
} else if (stat && stat.isDirectory()) {
306
// Find all children of the directory and queue those
307
fs.readdir(resolved, function(err, list) {
308
if (err) {
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
}
October 3, 2012 21:47
313
314
list.forEach(function(f) {
315
// For everything in the directory, queue it unless it looks hiden and we've
316
// been told to ignore hidden files.
317
if (self.options.ignoreHidden && f.charAt(0).match(/[\._]/)) return;
August 3, 2015 21:36
318
self.files.push(path.join(file, f));
319
});
320
cb();
321
});
323
// Wahey, we have a normal file. Go ahead and process it then.
August 3, 2015 21:36
324
self.processFile(file, cb);
325
}
326
});
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
*/
339
Docker.prototype.processFile = function(file, cb) {
August 3, 2015 21:36
340
var resolved = path.resolve(this.options.inDir, file);
341
var self = this;
342
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();
August 3, 2015 21:36
346
347
fs.readFile(resolved, 'utf-8', function(err, data) {
348
if (err) return cb(err);
August 3, 2015 21:36
349
350
// Grab the language details for the file and bail if we don't understand it.
August 3, 2015 21:36
351
var lang = self.detectLanguage(resolved, data);
352
if (lang === false) return cb();
August 3, 2015 21:36
353
354
self.addFileToTree(file);
355
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;
366
}
367
});
368
});
369
};
370
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
*/
385
Docker.prototype.decideWhetherToProcess = function(filename, callback) {
386
// If we should be processing all files, then yes, we should process this one
387
if (!this.options.onlyUpdated) return callback(true);
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) {
408
// If the output file doesn't exist, then definitely process this file
409
if (err && err.code == 'ENOENT') return callback(true);
411
fs.stat(file, function(err, inStat) {
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
*/
436
Docker.prototype.parseSections = function(data, lang) {
August 3, 2015 21:36
437
var lines = data.split('\n');
438
439
var section = {
440
docs: '',
441
code: ''
442
};
August 3, 2015 21:36
443
444
var sections = [];
445
446
var inMultiLineComment = false;
447
var multiLine = '';
448
var jsDocData;
August 3, 2015 21:36
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;
460
lines.forEach(function(line, i) {
461
// Only match against parts of the line that don't appear in strings
462
var matchable = line.replace(/(["'])((?:[^\\\1]|(?:\\\\)*?\\[^\\])*?)\1/g, '$1$1');
463
if (lang.literals) {
464
lang.literals.forEach(function(replace) {
465
matchable = matchable.replace(replace[0], replace[1]);
466
});
467
}
469
if (lang.multiLine) {
470
// If we are currently in a multiline comment, behave differently
471
if (inMultiLineComment) {
472
// End-multiline comments should match regardless of whether they're 'quoted'
473
if (line.match(lang.multiLine[1])) {
474
// Once we have reached the end of the multiline, take the whole content
475
// of the multiline comment, and parse it as jsDoc.
476
inMultiLineComment = false;
October 7, 2012 10:31
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); });
October 7, 2012 10:31
493
August 3, 2015 21:36
494
multiLine = stripIndent(multiLine);
October 7, 2012 10:31
495
496
if (lang.jsDoc) {
October 7, 2012 10:31
497
// Strip off leading * characters.
December 13, 2015 15:38
498
multiLine = multiLine.replace(/^[ \t]*\*? ?/gm, '');
500
jsDocData = dox.parseComment(multiLine, { raw: true });
501
502
// Put markdown parser on the data so it can be accessed in the template
August 3, 2015 21:36
503
jsDocData.md = mark;
504
section.docs += self.renderTemplate('jsDoc', jsDocData);
October 7, 2012 10:31
506
section.docs += '\n' + multiLine + '\n';
507
}
508
multiLine = '';
510
multiLine += line + '\n';
511
}
August 3, 2015 21:36
512
return;
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
518
// alert('foo'); // Alert some foo /* Random open comment thing
August 3, 2015 21:36
520
matchable.match(lang.multiLine[0]) &&
521
!matchable.replace(lang.multiLine[0], '').match(lang.multiLine[1]) &&
August 3, 2015 21:36
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);
527
section = { docs: '', code: '' };
528
}
529
inMultiLineComment = true;
December 13, 2015 15:38
530
multiLine = line + '\n';
August 3, 2015 21:36
531
return;
August 3, 2015 21:36
535
!self.options.multiLineOnly &&
536
lang.comment &&
537
matchable.match(commentRegex) &&
August 3, 2015 21:36
538
(!lang.commentsIgnore || !matchable.match(lang.commentsIgnore)) &&
539
!matchable.match(/#!/)
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';
547
} else if (!lang.commentsIgnore || !line.match(lang.commentsIgnore)) {
October 9, 2012 21:32
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
550
if (!section.firstCodeLine) {
October 9, 2012 21:32
551
section.firstCodeLine = i + 1;
552
}
553
section.code += line + '\n';
554
}
August 3, 2015 21:36
555
});
556
557
sections.push(section);
558
return sections;
559
};
560
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
*/
571
Docker.prototype.detectLanguage = function(filename, contents) {
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++
577
if (ext === '.C') return languages.cpp;
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 &&
586
languages[i].extensions.indexOf(ext) !== -1) return languages[i];
587
if (languages[i].names &&
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|$)/;
594
var match = shebangRegex.exec(contents);
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;
August 3, 2015 21:36
621
section.docHtml = md.render(section.docs);
622
});
623
};
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
*/
635
Docker.prototype.addAnchors = function(docHtml, idx, headings) {
636
var headingRegex = /<h(\d)(\s*[^>]*)>([\s\S]+?)<\/h\1>/gi; // toc.defaults.headers
637
638
if (docHtml.match(headingRegex)) {
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
641
docHtml = docHtml.replace(headingRegex, function(a, level, attrs, content) {
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>',
650
' </h' + level + '>',
651
'</div>'
652
].join('\n');
653
});
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
*/
676
Docker.prototype.addLineNumbers = function(html, first) {
677
var lines = html.split('\n');
678
679
lines = lines.map(function(line, i) {
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) {
August 3, 2015 21:36
699
var self = this;
701
var headings = { ids: {}, list: [] };
703
sections.forEach(function(section, i) {
704
// Add anchors to all headings in all sections
August 3, 2015 21:36
705
section.docHtml = self.addAnchors(section.docHtml, i, headings);
707
// Add line numbers of we need them
708
if (self.options.lineNums) {
August 3, 2015 21:36
709
section.codeHtml = self.addLineNumbers(section.codeHtml, section.firstCodeLine);
May 6, 2014 01:05
710
}
August 3, 2015 21:36
711
});
May 6, 2014 01:05
712
August 3, 2015 21:36
713
var content = this.renderTemplate('code', {
714
title: path.basename(filename),
715
sections: sections,
716
language: language.language
717
});
May 6, 2014 01:05
718
December 13, 2015 14:41
719
this.makeOutputFile(filename, content, headings, cb);
720
};
721
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
*/
732
Docker.prototype.renderMarkdownFile = function(data, filename, cb) {
733
var content = md.render(data);
734
735
var headings = { ids: {}, list: [] };
736
737
// Add anchors to all headings
738
content = this.addAnchors(content, 0, 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) {
December 13, 2015 14:41
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)
768
.replace(path.resolve(this.options.outDir), '')
769
.replace(/^[\/\\]/, '');
December 13, 2015 14:41
770
var levels = relativeOut == '' ? 0 : relativeOut.split(path.sep).length;
771
var relDir = repeating('../', levels);
772
773
// Render the html file using our template
August 3, 2015 21:36
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); })
August 3, 2015 21:36
783
});
December 13, 2015 14:41
785
// Recursively create the output directory, clean out any old version of the
786
// output file, then save our new file.
August 3, 2015 21:36
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
*/
796
Docker.prototype.copySharedResources = function() {
797
var self = this;
August 3, 2015 21:36
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
);
804
// Generate the CSS file using LESS. First, load the less file.
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
August 3, 2015 21:36
807
var hlpath = require.resolve('highlight.js');
808
var cspath = path.resolve(path.dirname(hlpath), '..', 'styles');
809
var colours = require('./getColourScheme')(self.options.colourScheme);
810
811
// Now compile the LESS to CSS
August 3, 2015 21:36
812
less.render(file.toString().replace('COLOURSCHEME', self.options.colourScheme), {
813
paths: [ cspath ],
August 3, 2015 21:36
814
globalVars: colours
815
}, function(err, out) {
816
// Now we've got the rendered CSS, write it out.
August 3, 2015 21:36
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) {
August 3, 2015 21:36
826
self.writeFile(
827
path.join(self.options.outDir, 'doc-script.js'),
828
file,
829
'Copied JS to doc-script.js'
830
);
831
});
833
this.options.js.concat(this.options.css).forEach(function(ext) {
August 3, 2015 21:36
834
var fn = path.basename(ext);
835
fs.readFile(path.resolve(ext), function(err, file) {
August 3, 2015 21:36
836
self.writeFile(path.join(self.options.outDir, fn), file, 'Copied ' + fn);
August 3, 2015 21:36
838
});
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
*/
850
Docker.prototype.outFile = function(filename) {
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
*/
864
Docker.prototype.renderTemplate = function(templateName, obj) {
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());
873
return this._templates[templateName](obj);
October 9, 2012 21:32
876
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();