UNPKG

course-renderer

Version:

Manages CA School Courses file system storage and HTML conversion

231 lines (230 loc) 8.57 kB
"use strict"; 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); } }); }