mdexam
Version:
Exam questions/answers/checkers written in markdown
483 lines (421 loc) • 12.6 kB
JavaScript
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['lodash'], factory);
} else if (typeof module === 'object' && module.exports) {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory(require('lodash'));
} else {
// Browser globals (root is window)
root.mdexam = factory(root.lodash);
}
}(this, function (lodash) {
function mdexam(value) {
// exit early if already wrapped, even if wrapped by a different `mdexam` constructor
if (value && typeof value == 'object' && value.__wrapped__) {
return value;
}
// allow invoking `mdexam` without the `new` operator
if (!(this instanceof mdexam)) {
return new mdexam(value);
}
this.__wrapped__ = value;
}
var _ = lodash;
// Define regular expressions to parse markdown
var regexQ = /##\s?\[(.*题)\]\s?(\S+)/;
var regexQSection = /####/g;
var regexTag = /\[标签\]/;
var regexChoice = /\[选项\]/;
var regexAnswer = /\[答案\]/;
var regexOptions = /\*\s{0,}(\S+)/g;
var regexChecker = /\[验证\]/;
var regexAnswerRegex = /\*\s\[answer-regex\]\s{0,}(\S+)/g;
var regexOutputRegex = /\*\s\[output-regex\]\s{0,}(\S+)/g;
// md存储markdown的文本内容
mdexam.md = {};
// json存储json的内容
mdexam.json = {};
/**
* 将markdown格式的选择题转为JSON
* @param mdQ
*/
mdexam.prototype.convertMultiplChoice2J = function (mdQ) {
var qj = {
type: 'multiple-choice',
question: mdQ.match(regexQ)[2],
tags: [],
options: [],
answer: []
};
var sections = mdQ.split(regexQSection);
_.each(sections, function (section, index) {
if (index === 0) {
return 0;
}
if (regexTag.test(section)) {
qj.tags = _.map(section.match(regexOptions), function (tag) {
return tag.replace(/^\*\s{0,}/, '');
});
}
if (regexChoice.test(section)) {
qj.options = _.map(section.match(regexOptions), function (option) {
return option.replace(/^\*\s{0,}/, '');
});
}
if (regexAnswer.test(section)) {
qj.answer = _.map(section.match(regexOptions), function (answer) {
return answer.replace(/^\*\s{0,}/, '');
});
}
});
return qj;
};
/**
* 将markdown格式的填空题转为JSON
* @param mdQ
*/
mdexam.prototype.convertFillIn2J = function (mdQ) {
var qj = {
type: 'fill-in',
question: mdQ.match(regexQ)[2],
answer: null,
tags: [],
checker: []
};
var sections = mdQ.split(regexQSection);
_.each(sections, function (section, index) {
if (index === 0) {
return 0;
}
if (regexAnswer.test(section)) {
qj.answer = _.map(section.match(regexOptions), function (tag) {
return tag.replace(/^\*\s{0,}/, '');
})[0];
}
if (regexTag.test(section)) {
qj.tags = _.map(section.match(regexOptions), function (tag) {
return tag.replace(/^\*\s{0,}/, '');
});
}
if (regexChecker.test(section)) {
qj.checker = _.map(section.match(regexAnswerRegex), function (regex) {
return {
'answer-regex': regex.replace(/\*\s{0,}\[answer-regex\]\s{0,}/, '')
}
});
}
});
return qj;
};
/**
* Convert command markdown question to JSON
* @param mdQ
*/
mdexam.prototype.convertCmd2J = function (mdQ) {
var qj = {
type: 'cmd-fill-in',
question: mdQ.match(regexQ)[2],
answer: null,
tags: [],
checker: []
};
var sections = mdQ.split(regexQSection);
_.each(sections, function (section, index) {
if (index === 0) {
return 0;
}
if (regexAnswer.test(section)) {
qj.answer = _.map(section.match(regexOptions), function (option) {
return option.replace(/^\*\s{0,}/, '');
})[0];
}
if (regexTag.test(section)) {
qj.tags = _.map(section.match(regexOptions), function (tag) {
return tag.replace(/^\*\s{0,}/, '');
});
}
if (regexChecker.test(section)) {
qj.checker = qj.checker.concat(_.map(section.match(regexAnswerRegex), function (regex) {
return {
'answer-regex': regex.replace(/\*\s{0,}\[answer-regex\]\s{0,}/, '')
}
}), _.map(section.match(regexOutputRegex), function (regex) {
return {
'output-regex': regex.replace(/\*\s{0,}\[output-regex\]\s{0,}/, '')
}
}));
}
});
return qj;
};
/**
* @TODO Convert code fragment markdown question to JSON
* @param mdQ
*/
mdexam.prototype.convertCodeFragment2J = function (mdQ) {
return null;
};
/**
* @TODO Convert on-line IDE fragment markdown question to JSON
* @param mdQ
*/
mdexam.prototype.convertProject2J = function (mdQ) {
return null;
};
mdexam.prototype._convert = function (mdQ) {
return null;
};
/**
* 根据markdown的内容,判断用什么转换函数转换为题目的json格式。
* @param mdQ
* @returns {*}
*/
mdexam.prototype.getConvertFunc = function (mdQ) {
var self = this;
if (!regexQ.test(mdQ)) {
console.warn("Malformed markdown question: ", mdQ);
return self._convert;
}
switch (mdQ.match(regexQ)[1]) {
case '选择题':
return self.convertMultiplChoice2J;
case '填空题':
return self.convertFillIn2J;
case '命令题':
return self.convertCmd2J;
case '代码片段题':
return self.convertCodeFragment2J;
case '项目工程题':
return self.convertProject2J;
default:
console.warn('Unknown question type: ', mdQ);
return self._convert;
}
};
/**
* Convert a single markdown question to json
* @param mdQ - Single question markdown
*/
mdexam.prototype.convertMdQ2J = function (mdQ) {
return this.getConvertFunc(mdQ)(mdQ);
};
/**
* Distill a list of questions(in markdown) from origin markdown
* @param mdString -
* @return Array - a list of markdown which is a single question.
*/
mdexam.prototype.distillQs = function (mdString) {
var regexQ = /##\s?\[(.*题)\]\s?(\S+)/g;
var qs = mdString.match(regexQ);
return _.map(qs, function (q, index) {
var start = mdString.indexOf(qs[index]);
var end = (index + 1) === qs.length ? (mdString.length - 1) : mdString.indexOf(qs[index + 1]);
return mdString.slice(start, end);
});
};
/**
* 将markdown转为json
* @param mdString - markdown文档
* @return json expression of
*/
mdexam.prototype.m2j = function (mdString) {
var self = this;
console.log('1----' + mdString);
// 挨个题目提取json
mdexam.json.questions = _.chain(self.distillQs(mdString))
.filter(function (mdq) {
return self.convertMdQ2J(mdq) !== null;
})
.map(function (mdq) {
return self.convertMdQ2J(mdq);
}).value();
// 提取外围信息
console.log('2----' + mdexam.json.questions);
var extraRegexes = {
title: /^#\s{0,}(\S+)/g,
author: /####\s{0,}\[作者\]\s{0,}(\S+)/g,
email: /####\s{0,}\[邮箱\]\s{0,}(\S+)/g,
version: /####\s{0,}\[版本\]\s{0,}(\S+)/g,
tags: /####\s{0,}\[标签\]\s{0,}([^#]+)/gm
};
console.log('3----' + extraRegexes);
_.each(extraRegexes, function (reg, k) {
if (k === 'tags') {
var tagsSection = reg.test(mdString) ? mdString.match(reg)[0] : "";
mdexam.json[k] = _.map(tagsSection.match(regexOptions), function (tag) {
return tag.replace(/^\*\s{0,}/, '');
});
return 0;
}
mdexam.json[k] = reg.test(mdString) ?
mdString.match(reg)[0].replace(/^####\s{0,}\[[^\]]+\]\s{0,}/g, '').replace(/^#\s{0,}/g, '') : null;
});
return mdexam.json;
};
/**
* 将json变为字符串链接起来
* @param arr -
* @param str -
* @return string.
*/
mdexam.prototype.connectStr = function (arr, str) {
if (arr == undefined) {
return
}
var mainStr = '';
mainStr += '#### [' + str + ']';
if (arr instanceof Array) {
_.each(arr, function (item) {
mainStr += '* ' + item;
})
} else {
mainStr += '* ' + arr;
}
return mainStr;
};
/**
* 判断是哪种类型的题
* @param arr -
* @param str -
* @return string.
*/
mdexam.prototype.judgeMdTyoe = function (questionType) {
switch (questionType) {
case 'multiple-choice':
return '选择题';
case 'fill-in':
return '填空题';
case 'cmd-fill-in':
return '命令题';
case '---':
return '代码片段题';
case '----':
return '项目工程题';
default:
console.warn('Unknown question type: ', mdQ);
return self._convert;
}
};
/**
* @TODO 将json转为markdown
*/
mdexam.prototype.j2m = function (jsonObj) {
var self = this;
//挨个题目转换为md
console.log('11');
var questions = JSON.parse(jsonObj);
_.each(questions['questions'], function (question) {
mdexam.md.questions += '## [' + mdexam.prototype.judgeMdTyoe(question.type) + '] ' + question.question;
mdexam.md.questions = mdexam.md.questions
+ mdexam.prototype.connectStr(question.tags, '标签')
+ mdexam.prototype.connectStr(question.options, '选择')
+ mdexam.prototype.connectStr(question.answer, '答案')
console.log('李静=======' + question.type);
});
console.log('22');
return mdexam.md.questions
};
/**
* @TODO 渲染JSON到模板
*/
mdexam.prototype.render = function () {
};
/**
* @TODO 自动评价选择题
* @param originQj
* @param testQj
* @return boolean true/false
*/
mdexam.prototype.checkChoice = function (originQj, testQj) {
if (!testQj || !testQj.answer) {
return false;
}
return _.isEqual(testQj.answer, originQj.answer);
};
/**
* @TODO 自动评价填空题
* @param originQj
* @param testQj
*/
mdexam.prototype.checkFillIn = function (originQj, testQj) {
if (!testQj || !testQj.answer) {
return false;
}
var result = false;
_.each(originQj.checker, function (checker) {
if (checker['answer-regex'] && eval(checker['answer-regex']).test(testQj.answer)) {
result = true;
return 0;
}
});
return result;
};
/**
* @TODO 自动评价命令题
* @param originQj
* @param testQj
*/
mdexam.prototype.checkCmdFillIn = function (originQj, testQj) {
if (!testQj || !testQj.answer) {
return false;
}
var result = false;
_.each(originQj.checker, function (checker) {
if (checker['answer-regex'] && eval(checker['answer-regex']).test(testQj.answer)) {
result = true;
return 0;
}
});
return result;
};
/**
* 根据json判断用什么打分函数对题目进行打分
* @param mdQ
* @returns {*}
*/
mdexam.prototype.judgeCheckFun = function ( originQj, testQj) {
var self = this;
switch (originQj.type) {
case 'multiple-choice':
return self.checkChoice(originQj, testQj);
case 'fill-in':
return self.checkFillIn(originQj, testQj);
case 'cmd-fill-in':
return self.checkCmdFillIn(originQj, testQj);
default:
console.warn('Unknown question type: ', originQj);
return false;
}
};
/**
* 自动评分
* @param originMd 出题试卷
* @param testMd 考生试卷
*/
mdexam.prototype.mdCheck = function (originMd, testMd) {
return this.jsonCheck(this.m2j(originMd), this.m2j(testMd));
};
/**
* @TODO 自动评分
* @param originJson
* @param testJson
*/
mdexam.prototype.jsonCheck = function (originJson, testJson) {
// @TODO 转换成题目索引的字典
var self = this;
var questionIndex = {};
_.each(originJson.questions,function (question) {
questionIndex[question.question] = question;
});
// @TODO 逐个题验证
_.each(testJson.questions,function (question) {
question.result = self.judgeCheckFun(questionIndex[question.question], question)
});
// @TODO 返回验证的结果
return testJson
};
return mdexam;
}));