UNPKG

course-renderer

Version:

Manages CA School Courses file system storage and HTML conversion

163 lines (136 loc) 4.88 kB
/** * @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); } }