UNPKG

9.56 kBJavaScriptView Raw
1'use strict';
2
3/*
4 * Parses the JavaScript files in `addon/` and
5 * creates documentation Markdown files for use in GitHub Pages.
6 * The docs files are written to the 'gh-pages' branch
7 * in the directory 'api/methods'.
8 */
9
10var fs = require('fs');
11var cp = require('child_process')
12var path = require('path');
13var ncp = require('ncp');
14var mkdirp = require('mkdirp');
15var rimraf = require('rimraf');
16var RSVP = require('rsvp');
17var packageJSON = require('./package.json');
18
19/* Runs an arbitrary shell command.
20 *
21 * @param {string} command - The shell command to execute
22 * @returns {Promise} (resolves {string}, rejects {Error}) A promise that resolves with the stdout of the command, or rejects with an Error that has the stderr as its message.
23 */
24function execCmd(command) {
25 return new RSVP.Promise(function(resolve, reject) {
26 var result;
27
28 console.log('> ' + command + '\n');
29 try {
30 result = cp.execSync(command);
31 resolve(result.toString());
32 } catch(error) {
33 reject(error);
34 }
35 });
36}
37
38/* Parses a source file for JSDoc comments.
39 *
40 * @param {string} filePath - The path to the source JavaScript file
41 * @returns {Promise} (resolves {string}) A promise that resolves with the Markdown text representation
42 */
43function parseSourceFile(filePath) {
44 return execCmd('node ./node_modules/documentation/bin/documentation build "' + filePath + '" -f md --shallow');
45}
46
47/* Takes Markdown and adds yml frontmatter and a table of contents, and adds 1 to
48 * the level of headings.
49 *
50 * @param {string} markdown - Unmassaged markdown.
51 * @returns {Promise} (resolves {string}) A promise that resolves with the massaged Markdown text representation.
52 */
53function massageMarkdown(markdown, options) {
54 var headerRegex = /^#{1,6} /;
55 var h2Regex = /^## (.*)$/;
56 var lines = markdown.split('\n');
57 var tableOfContents = [
58 '### Methods\n'
59 ];
60 // The jekyll yml frontmatter
61 var frontmatter = [
62 '---',
63 'layout: page',
64 'title: ' + options.title,
65 '---'
66 ];
67 var processedMarkdown;
68 var h2;
69 var tocLine;
70
71 function dasherize(str) {
72 return str.toLowerCase().replace(/[^\w]+/g, '-');
73 }
74
75 // For each line, if the line is a heading, increase its level by 1.
76 // (I.e., there shouldn't be any H1s in the Markdown.)
77 for (var i = 0; i < lines.length; i++) {
78 if (lines[i].match(headerRegex)) {
79 lines[i] = '#' + lines[i];
80 }
81
82 h2 = lines[i].match(h2Regex);
83
84 if (h2) {
85 // - [Header text](#header-text)
86 tocLine = '- [' + h2[1] + ']' + '(#' + dasherize(h2[1]) + ')';
87
88 tableOfContents.push(tocLine);
89 }
90 }
91
92 // Place the markdown inside a Liquid '{% raw %}{% endraw %}' block
93 // so that '{{component-name}}' hbs tags are rendered in code blocks.
94 // (Liquid will parse them as Liquid tags otherwise.)
95 processedMarkdown = frontmatter.join('\n') + '\n\n' +
96 '{% raw %}\n' +
97 tableOfContents.join('\n') + '\n\n' +
98 lines.join('\n') +
99 '{% endraw %}';
100
101 return new RSVP.Promise(function(resolve) {
102 resolve(processedMarkdown);
103 });
104}
105
106/* Write a documentation Markdown file.
107 *
108 * @param {string} srcPath - The full path of the source file or directory.
109 * @param {string} destDir - The directory in which to write the Markdown file.
110 * @param {Object} options
111 * @param {string} options.slug - The slug of the documentation. Used to construct the filename.
112 * @param {string} options.title - The page title of the documentation.
113 * @returns {Promise}
114 */
115function writeDocsFile(srcPath, destDir, options) {
116 // capture stdout of 'documentation'
117 // FIXME: Change this to use documentation Node API
118 var filename = options.slug + '.md';
119 var destPath = path.join(destDir, filename);
120 var promises = [];
121 var filePath;
122 var files;
123
124 if (fs.statSync(srcPath).isDirectory()) {
125 // If the path is a directory,
126 // get a list of files in the directory
127 files = fs.readdirSync(srcPath);
128 } else {
129 // Otherwise, the path is a file name
130 files = [srcPath];
131 }
132
133 for (var i = 0; i < files.length; i++) {
134 if (files[0] !== srcPath) {
135 // If it's just a filename, add the path
136 filePath = path.join(srcPath, files[i]);
137 } else {
138 filePath = srcPath;
139 }
140
141 // Only try to parse files
142 if (fs.statSync(filePath).isFile()) {
143 promises.push(parseSourceFile(filePath));
144 }
145 }
146
147 return RSVP.all(promises)
148 .then(function(markdownArray) {
149 return massageMarkdown(markdownArray.join('\n'), options);
150 })
151 .then(function(markdown) {
152 return new RSVP.Promise(function(resolve, reject) {
153 // use {'flags': 'a'} to append and {'flags': 'w'} to erase and write a new file
154 var stream = fs.createWriteStream(destPath, { flags: 'w' });
155
156 stream.on('finish', resolve);
157 stream.on('error', reject);
158
159 stream.write(markdown);
160
161 stream.end();
162
163 console.log('Wrote Markdown to file ' + destPath);
164 });
165 });
166}
167
168/* Write documentation for the files in a directory.
169 *
170 * Individual files in the directory will each have their own documentation
171 * files. Subdirectories will group the docs for all files together.
172 *
173 * @param {string} srcDir - The directory in which the source files are contained.
174 * @param {string} destDir - The directory in which to write the documentation files.
175 * @returns {Promise}
176 */
177function writeDocs(srcDir, destDir) {
178 var promises = [];
179 var sources;
180 var srcPath;
181 var stat;
182 var options;
183 var slug;
184
185 function capitalizeFirstLetter(string) {
186 return string.charAt(0).toUpperCase() + string.slice(1);
187 }
188
189 if (fs.statSync(srcDir).isDirectory()) {
190 // If the path is a directory,
191 // get a list of files in the directory
192 sources = fs.readdirSync(srcDir);
193 } else {
194 return new RSVP.Promise(function(resolve, reject) {
195 reject('Must pass a directory to writeDocs');
196 });
197 }
198
199 for (var i = 0; i < sources.length; i++) {
200 srcPath = path.join(srcDir, sources[i]);
201 stat = fs.statSync(srcPath);
202 slug = sources[i].split('.')[0];
203
204 options = {
205 slug: slug,
206 title: capitalizeFirstLetter(slug.replace('-', ' '))
207 }
208
209 if (stat.isFile() || stat.isDirectory()) {
210 promises.push(writeDocsFile(srcPath, destDir, options));
211 }
212 }
213
214 return RSVP.all(promises)
215}
216
217/* Copies the documentation files from the temporary directory to the Jekyll
218 * docs directory.
219 *
220 * @param {string} srcDir - The directory in which the source files are contained.
221 * @param {string} destDir - The directory in which to write the documentation files.
222 * @returns {Promise}
223 */
224function copyDocs(srcDir, destDir) {
225 return new RSVP.Promise(function(resolve, reject) {
226 // Copy the docs directory
227 ncp(srcDir, destDir, function (err) {
228 if (err) {
229 reject(err);
230 } else {
231 console.log('Copied docs to ' + destDir);
232 resolve();
233 }
234 });
235 });
236}
237
238/* Creates a directory (and its parent directories, if necessary).
239 *
240 * @param {string} dir - The directory to create.
241 * @returns {Promise}
242 */
243function createDir(dir) {
244 return new RSVP.Promise(function(resolve, reject) {
245 // Delete the temporary compiled docs directory.
246 mkdirp(dir, function (err) {
247 if (err) {
248 reject(err);
249 } else {
250 console.log('Created directory ' + dir);
251 resolve();
252 }
253 });
254 });
255}
256
257/* Deletes a directory recursively.
258 *
259 * @param {string} dir - The directory to delete.
260 * @returns {Promise}
261 */
262function removeDir(dir) {
263 return new RSVP.Promise(function(resolve, reject) {
264 // Delete the temporary compiled docs directory.
265 rimraf(dir, function (err) {
266 if (err) {
267 reject(err);
268 } else {
269 console.log('Removed directory ' + dir);
270 resolve();
271 }
272 });
273 });
274}
275
276(function() {
277 var versionArray = packageJSON.version.split('.');
278 // ex., '1.0.3' -> 'v1.0.x'
279 var version = 'v' + versionArray[0] + '.' + versionArray[1] + '.x';
280
281 var propertiesDir = path.join(__dirname, 'addon/-private/properties');
282 var tmpDir = path.join(__dirname, 'tmp_docs');
283 var destDir = path.join(__dirname, 'docs', version, 'api');
284
285 // Create the temporary directory for the docs
286 createDir(tmpDir)
287 .then(function() {
288 // Create the documentation files
289 return writeDocs(propertiesDir, tmpDir);
290 })
291 .then(function() {
292 return execCmd('git branch');
293 })
294 .then(function(branches) {
295 const branch = 'gh-pages';
296
297 // Switch to the GitHub Pages branch
298 if (branches.includes(branch)) {
299 return execCmd(`git checkout ${branch}`);
300 } else {
301 return execCmd(`git checkout -b ${branch}`);
302 }
303 })
304 .then(function() {
305 // Delete the existing destination directory
306 return removeDir(destDir);
307 })
308 .then(function() {
309 // Create the destination directory
310 return createDir(destDir);
311 })
312 .then(function() {
313 // Copy the docs to the destination directory
314 return copyDocs(tmpDir, destDir);
315 })
316 .then(function() {
317 // Delete the temporary directory
318 return removeDir(tmpDir);
319 })
320 .then(function() {
321 console.log('Finished writing documentation files.');
322 })
323 .catch(function(reason) {
324 console.log(reason.stack);
325 });
326})();