course-renderer
Version:
Manages CA School Courses file system storage and HTML conversion
231 lines (230 loc) • 8.57 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const vfs = require("vinyl-fs");
const Orchestrator = require("orchestrator");
const through = require("through2");
const path = require("path");
const fs = require("fs-extra");
const del = require("del");
const md2json = require('./tasks/md2json');
const summaryParser = require('./tasks/summary-parser');
const postRenderer = require('./tasks/post-renderer');
const customRendering = require('./tasks/custom-rendering');
/**
* Renders a course chapter file.
*
* @param sourceFile path to the source file to be rendered.
* @param cb callback to be called when the rendering is done.
* Accepts two argument, the error and the rendered string.
*/
function renderChapter(sourceFile, cb) {
const errors = [];
const output = [];
vfs.src(sourceFile)
.pipe(md2json({ "sourcePath": sourceFile }))
.on('error', (error) => {
errors.push(error.message);
})
.on('data', (chunk) => {
const content = chunk.contents.toString();
output.push(content);
})
.on('finish', () => {
if (errors && errors.length > 0)
return cb(errors);
const sep = process.platform === 'win32' ? '\r\n' : '\n';
cb(null, output.join(sep));
});
}
exports.renderChapter = renderChapter;
/** TODO There's too much code duplicate on this module. */
/**
* Renders the course content ONLY, this is mostly use by the validator. Since the validator only concern is the course content directory and the summary.md file
*
* @param source path to the directory where all course raw directory are stored
* @param dest path to the directory where all course rendered directory are stored
* @param course the name of the course to be rendered
* @param silent if we're on silent mode. This will suppress output.
* @param cb callback when the rendering is done or if an error occur.
*/
function renderCourseContent(source, dest, course, silent, cb) {
const orchestrator = new Orchestrator();
const sourceCourse = path.resolve(source, course);
const destCourse = path.resolve(dest, course);
let error = '';
/**
* The course content rendering, transforming markdown files to html files
*/
orchestrator.add('course-content', () => {
return vfs.src([`${sourceCourse}/content/**/*.md`])
.pipe(md2json({ "sourcePath": source }))
.on('error', (err) => {
error = err.message;
orchestrator.stop();
})
.pipe(vfs.dest(path.resolve(destCourse, 'content')));
});
/**
* Cleanup html files. And generate answer.yml file
*/
orchestrator.add('post-render-course-content', ['course-content'], () => {
return vfs.src(`${destCourse}/content/**/*.json`)
.pipe(postRenderer({ "course": course, "dest": destCourse, "sourceCourse": sourceCourse }))
.on('error', (err) => {
error = err.message;
orchestrator.stop();
})
.pipe(vfs.dest(path.resolve(destCourse, 'content')));
});
/**
* Transform the SUMMARY.md file to SUMMARY.json
*/
orchestrator.add('summary', () => {
return vfs.src(`${sourceCourse}/SUMMARY.md`)
.pipe(summaryParser())
.on('error', (err) => {
error = err.message;
orchestrator.stop();
})
.pipe(vfs.dest(destCourse));
});
/**
* Copy the tests directory to the rendered directory
*/
orchestrator.add('tests', ['course-content'], () => {
return vfs.src(`${sourceCourse}/tests/**/*`)
.on('error', (err) => {
error = err.message;
orchestrator.stop();
})
.pipe(vfs.dest(path.resolve(destCourse, 'tests')));
});
orchestrator.start('course-content', 'post-render-course-content', 'tests', 'summary', () => {
cb(error);
});
if (!silent) {
orchestrator.on('task_stop', (event) => {
const duration = event.duration * 1000;
console.log(`Rendering ${event.task} ${duration.toFixed(2)}ms`);
});
}
}
exports.renderCourseContent = renderCourseContent;
function customRender(target, extension, silent, cb) {
vfs.src(`${target}/content/**/*.html`)
.pipe(customRendering({ "extension": extension }))
.on('error', (err) => {
cb(err);
})
.pipe(vfs.dest(target))
.on('finish', cb);
}
exports.customRender = customRender;
/**
* Renders a course and save it to a destination path.
*
* @param source path to the directory where all course raw directory are stored
* @param dest path to the directory where all course rendered directory are stored
* @param course the name of the course to be rendered
* @param silent if we're on silent mode. This will suppress output.
* @param cb callback when the rendering is done or if an error occur.
*/
function render(source, dest, course, silent, cb) {
const orchestrator = new Orchestrator();
const sourceCourse = path.resolve(source, course);
const destCourse = path.resolve(dest, course);
let error = '';
/**
* Clear the course on the dest directory, if it exists.
*/
orchestrator.add('cleanup', () => {
return del(`${destCourse}/**/*`, { force: true });
});
/**
* The course content rendering, transforming markdown files to JSON files
*/
orchestrator.add('course-content', ['cleanup'], () => {
return vfs.src([`${sourceCourse}/content/**/*md`])
.pipe(md2json({ "sourcePath": source }))
.on('error', (err) => {
error = err.message;
orchestrator.stop();
})
.pipe(vfs.dest(path.resolve(destCourse, 'content')));
});
/**
* Cleanup files. And generate answer.yml file
*/
orchestrator.add('post-render-course-content', ['course-content'], () => {
return vfs.src(`${destCourse}/content/**/*.json`)
.pipe(postRenderer({ "course": course, "dest": destCourse, "sourceCourse": sourceCourse }))
.on('error', (err) => {
error = err.message;
orchestrator.stop();
})
.pipe(vfs.dest(path.resolve(destCourse, 'content')));
});
/**
* Transform the SUMMARY.md file to SUMMARY.json
*/
orchestrator.add('summary', ['cleanup'], () => {
return vfs.src(`${sourceCourse}/SUMMARY.md`)
.pipe(summaryParser())
.on('error', (err) => {
error = err.message;
orchestrator.stop();
})
.pipe(vfs.dest(destCourse));
});
/**
* Copies other course content to the rendered course directory via stream. This does not copy symlink files
*/
orchestrator.add('others', ['cleanup'], () => {
return vfs.src([`${sourceCourse}/**/*`, `!${sourceCourse}/content/**/*`, `!${sourceCourse}/SUMMARY.md`])
.pipe(filterSymlinks())
.on('error', (err) => {
error = err.message;
orchestrator.stop();
})
.pipe(vfs.dest(destCourse));
});
/**
* Copies all symlink files to the rendered course directory via fs copy.
*/
orchestrator.add('symlinks', ['cleanup'], () => {
return vfs.src([`${sourceCourse}/**/*`, `!${sourceCourse}/content/**/*`, `!${sourceCourse}/SUMMARY.md`], { read: false })
.pipe(copySymlinks({ "source": sourceCourse, "dest": destCourse }));
});
orchestrator.start(['cleanup', 'course-content', 'post-render-course-content', 'summary', 'others', 'symlinks'], () => {
cb(error);
});
if (!silent) {
orchestrator.on('task_stop', (event) => {
const duration = event.duration * 1000;
console.log(`Rendering ${event.task} ${duration.toFixed(2)}ms`);
});
}
}
exports.render = render;
function filterSymlinks() {
return through.obj((file, enc, callback) => {
if (!file.stat.isSymbolicLink()) {
return callback(null, file);
}
else {
return callback();
}
});
}
function copySymlinks(options) {
return through.obj((file, enc, callback) => {
if (file.stat.isSymbolicLink()) {
fs.copy(file.path, path.resolve(file.path.replace(options.source, options.dest)), (err) => {
callback(err, file);
});
}
else {
callback(null, file);
}
});
}