course-renderer
Version:
Manages CA School Courses file system storage and HTML conversion
248 lines (223 loc) • 8.65 kB
text/typescript
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<string> = []
const output: Array<string> = []
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)
}
})
}