import * as vfs from 'vinyl-fs' import * as Orchestrator from 'orchestrator' import * as through from 'through2' import * as path from 'path' import * as fs from 'fs-extra' import * as del from '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. */ export function renderChapter(sourceFile: string, cb: any) { const errors: Array = [] const output: Array = [] vfs.src(sourceFile) .pipe(md2json({ "sourcePath": sourceFile })) .on('error', (error: any) => { errors.push(error.message) }) .on('data', (chunk: any) => { 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)) }) } /** 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. */ export function renderCourseContent(source: string, dest: string, course: string, silent: boolean, cb: any) { 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: any) => { 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: any) => { 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: any) => { 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: any) => { 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`) }) } } export function customRender(target: string, extension: any, silent: boolean, cb: any) { vfs.src(`${target}/content/**/*.html`) .pipe(customRendering({ "extension": extension })) .on('error', (err: any) => { cb(err) }) .pipe(vfs.dest(target)) .on('finish', cb) } /** * 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. */ export function render(source: string, dest: string, course: string, silent: boolean, cb: any) { 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: any) => { 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: any) => { 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: any) => { 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: any) => { 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`) }) } } function filterSymlinks() { return through.obj((file, enc, callback) => { if (!file.stat.isSymbolicLink()) { return callback(null, file) } else { return callback() } }) } function copySymlinks(options: any) { 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) } }) }