course-renderer
Version:
Manages CA School Courses file system storage and HTML conversion
163 lines (136 loc) • 4.88 kB
text/typescript
/**
* @module answer-generator
*/
import * as fs from 'fs-extra'
import * as yaml from 'js-yaml'
import * as path from 'path'
import * as crypto from 'crypto'
import * as _ from 'lodash'
import Token from '../markdown/Token'
const replaceExtension = require('replace-ext')
const { promisify } = require('util')
const read = promisify(fs.readFile)
const append = promisify(fs.appendFile)
const statFile = promisify(fs.stat);
const access = promisify(fs.access);
const readDir = promisify(fs.readdir);
interface AnswerMeta {
text: string,
chapter: string,
answer?: string,
id?: string,
type: string,
hint?: any,
provisionedId?: string
}
/**
* answerGenerator - Generates an answer.yml file for each course
*/
export async function answerGenerator(tokens: Token[], courseDest: string, courseSource: string, callback: any ) {
const pathSep = path.sep
const output = path.resolve(courseDest, 'answer.yml')
let answerOutput: any = {}
try {
let mapping;
try {
await access(path.resolve(courseSource, 'mapping.json'), fs.constants.F_OK);
mapping = JSON.parse(await read(path.resolve(courseSource, 'mapping.json'), 'utf-8'));
} catch(e) {
mapping = {};
}
await createTokensAnswer(tokens, answerOutput, courseSource, mapping)
if (!_.isEmpty(answerOutput)) {
await append(output, yaml.safeDump(answerOutput))
}
callback(null, tokens)
} catch(e) {
callback(e)
}
}
async function createTokensAnswer(tokens: Token[], answerOutput: any, courseSource: string, mapping: any) {
for (let token of tokens) {
if (token.children instanceof Array) {
await createTokensAnswer(token.children, answerOutput, courseSource, mapping)
} else if (token.type && token.type !== 'text' && token.type !== 'REPL') { // Do not generate questionId for text and REPL
const answer = createTokenAnswer(token)
if (token.type === 'CR') {
answer.hint = await getQuestionHint(courseSource, answer.answer, token.id);
} else if (token.type === 'MS') {
// Sort MS answer just in case the authors did not sort the answer on the annotation. The reader is expecting a sorted answer
const answers = answer.answer.split(',')
answers.sort((a: any, b: any) => {
return a - b
})
answer.answer = answers.join(',');
}
delete token.answer
if (mapping[token.id]) {
answer.provisionedId = mapping[token.id];1
}
answerOutput[token.id] = answer
}
}
}
function createTokenAnswer(token: Token): AnswerMeta {
return {
text: token.content,
chapter: token.chapter,
answer: token.answer,
type: token.type
}
}
async function readHintFile(hintFile: string) {
try {
return await read(hintFile, 'utf-8')
} catch (e) {
console.log(e);
throw new Error(`Unable to read Proof file ${hintFile}: ${e.message}`)
}
}
async function hintFileExists(hintFile: string) {
try {
const stat = await statFile(hintFile);
return true;
} catch(e) {
return false;
}
}
async function readHintDirectory(hintDirectory: string) {
try {
const files = await readDir(hintDirectory);
if (files.length == 0) {
throw new Error(`Hint directory ${hintDirectory} is empty`);
}
const hints: any = [];
for (let i = 0; i < files.length; i++) {
const file = path.resolve(hintDirectory, files[i]);
const content = await read(file, 'utf-8');
hints.push({ fileName: files[i], answer: content });
}
return hints;
} catch(e) {
throw e;
}
}
async function getQuestionHint(source: string, answerPath: string, id: string) {
let hintFile = path.resolve(source, answerPath.replace(/^tests/, 'answers'));
if (await hintFileExists(hintFile)) {
const fileName = path.basename(hintFile);
return [{ fileName: fileName, answer: await readHintFile(hintFile) }];
} else if (await hintFileExists(replaceExtension(hintFile, ''))) {
hintFile = replaceExtension(hintFile, '');
const fileName = path.basename(hintFile);
return [{ fileName: fileName, answer: await readHintFile(hintFile) }];
} else {
// Assume the hint is in a directory
const hintPath = path.resolve(
path.dirname(
path.resolve(
source, answerPath.replace(/^tests/, 'answers')
)
),
id
);
return await readHintDirectory(hintPath);
}
}