2021年当我想做一个兼容各操作系统的Anki选择题模板的时候,到处搜索茧中网,根本找不到相关内容,直到偶然在github上看到Simon Lammer的Anki持久化模块,才算真正实现。现在再在茧中网上搜索兼容各种操作系统的Anki选择题模板,已经有很多结果了——我不免猜想是不是直接或间接从我这里得了点启示?如果真得了启示,也不用声明,不妨留个赞。^_^^_^只想拷贝卡片模版内容的可直接滚动到文末,无需看前面的更新说明。
后来看到一个anki插件Edit Field During Review(安装代码:1020366288),提供了在复习界面修改修改笔记内容的功能,十分方便。我对这个插件做了一点微小的修改,使它在复习界面修改笔记时不但可以修改文本内容,还支持用HTML标签改变文本的显示样式,插入表格和图片等,详见anki插件Edit Field During Review的使用及改造。当时有人说还是不够方便,因为还要靠手工输入HTML代码,如果直接有按钮一点就实现文字样式的修改那就好了。今年刚好又打算考试,于是弄个Anki题库帮助学习,复习的时候修改笔记,很多时候只是想把某些文字加个粗换个颜色突出显示一下,输入HTML代码确实比较麻烦,因此对原来的模板进行了更新,实现了在复习界面按几下按钮就改变被选择的文本的显示样式的功能。演示见下图:
目前,这个选择题模板设置有五个字段:Question——选择题题干;Options——选择题选项,Answer——正确答案;Extra——解析;Section——相关知识点所属教材章节。要让上面的演示图片中做的样式修改永久保存到笔记中,必须安装anki插件Edit Field During Review(安装代码:1020366288),并且必须要按照anki插件Edit Field During Review的使用及改造这篇文章的介绍修改插件的源代码,并且在模版中的字段前增加“edit:”过滤器,否则复习时的修改不能保存到笔记中。当然,如果不需要将复习时的修改保存到笔记中,就无需这个插件了。
除了插件支持外,笔记卡片的样式文件要包含如下内容:
/*说明:以下用u,a,i,b四个标签和4个css类定义相同的样式。
卡片背面模板中创建的临时HTML元素可以是u,a,i,b,也可以是span。
创建span元素时为不同样式指定不同类名,创建u,a,i,b时无需指定类名。
创建u,a,i,b时笔记最终保存的HTML字符串较短。
*/
u, .red{
color:red;
font-weight: bold;
text-decoration:none;
}
.orange, a,a:visited,a:hover,a:link,a:active{
color:#F90;
font-weight:bold;
}
.green, i{
font-weight: bold;
font-style:normal;color: #0f0;
}
.blue, b{
font-weight: bold;
font-style:normal;color: #3cf;
}
卡片背面内容模版中添加以下HTML代码增加4个按钮:
<div id='styleButtons'>
<button id="blue_bold">蓝色加粗</button>
<button id="red_bold">红色加粗</button>
<button id="green_bold">绿色加粗</button>
<button id="orange_bold">橙色加粗</button>
</div>
用于为以上按钮提供样式修改功能的JavaScript代码如下:
/*以下对在复习界面中选择的文本的显示样式进行处理*/
var styleButtonDiv = document.getElementById('styleButtons');
var styleButtons = styleButtonDiv.getElementsByTagName('button');
for(i=0;i<styleButtons.length;i++){
styleButtons[i].onclick=function(ev){
var selection = document.getSelection();
var selectStr = selection.toString();
if (selectStr.trim() != '') {
var rang = selection.getRangeAt(0);
//卡片样式如果用u,b,i,a等标签名定义,只需声明tmpEle
var tmpEle
//卡片样式如果用.red之类的类名定义,可创建span元素
//var tmpEle = document.createElement('span');
var eleId=ev.target.getAttribute('id')//获取触发事件的元素的id
if(eleId=='blue_bold'){//如果是蓝色加粗按钮
//用标签名定义样式,需创建相应样式的元素
tmpEle = document.createElement('b');
//用类名定义样式,为前面创建的span元素指定类名
//tmpEle.className = 'blue';
}else if(eleId=='red_bold'){//如果是红色加粗按钮
//用标签名定义样式,需创建相应样式的元素
tmpEle = document.createElement('u');
//用类名定义样式,为前面创建的span元素指定类名
//tmpEle.className = 'red';
}else if(eleId=='green_bold'){//如果是绿色加粗按钮
//用标签名定义样式,需创建相应样式的元素
tmpEle = document.createElement('i');
//用类名定义样式,为前面创建的span元素指定类名
//tmpEle.className = 'green';
}else{//如果是橙色加粗按钮
//用标签名定义样式,需创建相应样式的元素
tmpEle = document.createElement('a');
//用类名定义样式,为前面创建的span元素指定类名
//tmpEle.className = 'orange';
}
//将选中的文本插入临时元素内部
rang.surroundContents(tmpEle);
rang.deleteContents(); // 删除选中内容
rang.insertNode(tmpEle); //插入临时元素替换内容
} else {
alert('请先选择文本!');
}
}
}
最后,将整个选择题卡片模板文件提供如下:
正面内容模板:
<script>
// v1.1.8 - https://github.com/SimonLammer/anki-persistence/blob/584396fea9dea0921011671a47a0fdda19265e62/script.js
if(void 0===window.Persistence){var e="github.com/SimonLammer/anki-persistence/",t="_default";if(window.Persistence_sessionStorage=function(){var i=!1;try{"object"==typeof window.sessionStorage&&(i=!0,this.clear=function(){for(var t=0;t<sessionStorage.length;t++){var i=sessionStorage.key(t);0==i.indexOf(e)&&(sessionStorage.removeItem(i),t--)}},this.setItem=function(i,n){void 0==n&&(n=i,i=t),sessionStorage.setItem(e+i,JSON.stringify(n))},this.getItem=function(i){return void 0==i&&(i=t),JSON.parse(sessionStorage.getItem(e+i))},this.removeItem=function(i){void 0==i&&(i=t),sessionStorage.removeItem(e+i)},this.getAllKeys=function(){for(var t=[],i=Object.keys(sessionStorage),n=0;n<i.length;n++){var s=i[n];0==s.indexOf(e)&&t.push(s.substring(e.length,s.length))}return t.sort()})}catch(n){}this.isAvailable=function(){return i}},window.Persistence_windowKey=function(i){var n=window[i],s=!1;"object"==typeof n&&(s=!0,this.clear=function(){n[e]={}},this.setItem=function(i,s){void 0==s&&(s=i,i=t),n[e][i]=s},this.getItem=function(i){return void 0==i&&(i=t),void 0==n[e][i]?null:n[e][i]},this.removeItem=function(i){void 0==i&&(i=t),delete n[e][i]},this.getAllKeys=function(){return Object.keys(n[e])},void 0==n[e]&&this.clear()),this.isAvailable=function(){return s}},window.Persistence=new Persistence_sessionStorage,Persistence.isAvailable()||(window.Persistence=new Persistence_windowKey("py")),!Persistence.isAvailable()){var i=window.location.toString().indexOf("title"),n=window.location.toString().indexOf("main",i);i>0&&n>0&&n-i<10&&(window.Persistence=new Persistence_windowKey("qt"))}}</script>
<!--正面模板-->
<div class="text" id="question">{{Question}}<span class='imp'>【所属章节:{{Section}}】<span></div>
<ol class="options" id="optionList"></ol>
<div id="options" style="display:none">{{Options}}</div>
<div id="answer" style="display:none">{{text:Answer}}</div>
<script>
var myinfo;
//if (Persistence.isAvailable()) {
myinfo = Persistence.getItem();
if (myinfo == null) {
myinfo = {
single: 0, //本次已做全部练习题中单选题数量
singleCorrect: 0, //本次已做全部练习题中单选题正确数量
multi: 0, //本次已做全部练习题中多选题数量
multiCorrect: 0, //本次已做全部练习题中多选题完全正确数量
partCorrect: 0, //本次已做全部练习题中多选部分正确数量
multiScore: 0, //本次已做全部练习题中多选题得分
score: 0, //当前所作练习题得分
sum: 0, //本次已做全部练习题累计得分
total: 0, //本次已做练习总数量
totalScore: 0, //本次已做练习满分
newOrderOps: [], //当前所作练习题打乱顺序后的选项
newOrderAnswer: '', //当前所作练习题打乱选项顺序后新的正确答案编号
choiced: '', //当前所作练习题选中的选项
ifright: '' //当前所作练习题选中的选项是否正确
};
}
myinfo.total++;
myinfo.choiced = '';
myinfo.newOrderAnswer = '';
myinfo.newOrderOps = [];
var question = document.getElementById("question");
//读入答案,去掉多余字符和空格
var correctAnswer = document.getElementById('answer').innerHTML
.toUpperCase().replace(/[^A-Z]+/, "");
if (correctAnswer.length > 1) { //正确答案大于一个为多选题
myinfo.totalScore += 2;
myinfo.multi++;
question.innerHTML = "<span class='imp'>【多选题】</span>" + question.innerHTML;
} else { //单选题
myinfo.totalScore++;
myinfo.single++;
question.innerHTML = "<span class='imp'>【单选题】</span>" + question.innerHTML;
}
var options = document.getElementById("options"),
optionList = document.getElementById("optionList");
var s = 0;
var indexs = [];
//处理原始顺序的选项,将div标签和br标签以及多余的换行替换掉
var options = options.innerHTML;
options = options.replace(/<\/?div>/g, "\n");
options = options.replace(/\n+/g, "\n");
options = options.replace(/<br.*?>/g, "\n");
options = options.replace(/^\n/, "");
options = options.replace(/\n$/, "");
//以换行符分隔选项为数组
options = options.split("\n");
//随机组合选项
for (var op in options) {
//随机产生一个索引,如果产生的索引已处理过,继续产生下一个索引,没处理过就中断循环开始处理
do {
s = Math.random() * (options.length);
s = Math.floor(s);
if (indexs.join().indexOf(s.toString()) == -1) {
indexs.push(s);
myinfo.newOrderOps.push(options[s]);
break;
}
} while (true);
//将随机产生的选项组合成li包着的input和label
list = document.createElement("li");
label = document.createElement("label");
label.innerHTML = options[s];
var input = document.createElement("input");
//根据答案字符长短判定应该用多选框还是单选框
input.type = correctAnswer.length > 1?"checkbox":"radio";
input.value = s;
input.name = "opts";//将选项成组,以防单选题可选择多个选项
input.id = "opts_" + s;
label.for = "opts_" + s;
list.addEventListener("click", clickOption);
list.appendChild(input);
list.appendChild(label);
optionList.appendChild(list);
}
for (i = 0; i < options.length; i++) {
//将正确答案的字母序号转换成打乱顺序后的字母编号,并记录到myinfo.newOrderAnswer中
if (correctAnswer.indexOf(String.fromCharCode(65 + indexs[i])) >= 0) {
myinfo.newOrderAnswer += String.fromCharCode(65 + i);
}
}
Persistence.setItem(myinfo);
//在选项li标签所在区域点击时,实际触发事件的可能是li、label或者input组件,无论是那个组件,都定位到checkbox
function clickOption(ev) {
var checkbox = ev.target;
var tagName = checkbox.tagName;
if (tagName == 'LI') {
checkbox = checkbox.children[0];
} else if (tagName == 'LABEL') {
checkbox = checkbox.parentNode.children[0];
}
checkbox.checked = 'checked';
var s = checkbox.value;
//在打乱顺序后的索引数组中找到选项的新数字序号,再转换成对应的字母编号
var ch = String.fromCharCode(65 + indexs.join('').indexOf(s.toString()));
if (myinfo.choiced.indexOf(ch) == -1) {
if(correctAnswer.length > 1){//多选题,在已选择项上加上一个新选项
myinfo.choiced += ch;
}else{//单选题,将已选择项变更为刚选的选项
myinfo.choiced = ch;
}
} else { //点击已选中的选项则取消该选项的选中状态
myinfo.choiced = myinfo.choiced.replace(ch, '');
checkbox.checked = null;
}
//if (Persistence.isAvailable()) {
Persistence.setItem(myinfo);
/*} else {
window.myinfo = myinfo;
}*/
//根据选项是否被选择赋予不同的显示样式
for (var j=0;j<optionList.children.length;j++) {
var ch = String.fromCharCode(65 + j)
if (myinfo.choiced.indexOf(ch) == -1) {
optionList.children[j].className = "unchoiced";
} else {
optionList.children[j].className = "choiced";
}
}
}
/*} else {//无法持久化js对象,只能针对单面单题练习
}*/
</script>
背面内容模版:
<!--背面模板-->
<script>
// v1.1.8 - https://github.com/SimonLammer/anki-persistence/blob/584396fea9dea0921011671a47a0fdda19265e62/script.js
if(void 0===window.Persistence){var e="github.com/SimonLammer/anki-persistence/",t="_default";if(window.Persistence_sessionStorage=function(){var i=!1;try{"object"==typeof window.sessionStorage&&(i=!0,this.clear=function(){for(var t=0;t<sessionStorage.length;t++){var i=sessionStorage.key(t);0==i.indexOf(e)&&(sessionStorage.removeItem(i),t--)}},this.setItem=function(i,n){void 0==n&&(n=i,i=t),sessionStorage.setItem(e+i,JSON.stringify(n))},this.getItem=function(i){return void 0==i&&(i=t),JSON.parse(sessionStorage.getItem(e+i))},this.removeItem=function(i){void 0==i&&(i=t),sessionStorage.removeItem(e+i)},this.getAllKeys=function(){for(var t=[],i=Object.keys(sessionStorage),n=0;n<i.length;n++){var s=i[n];0==s.indexOf(e)&&t.push(s.substring(e.length,s.length))}return t.sort()})}catch(n){}this.isAvailable=function(){return i}},window.Persistence_windowKey=function(i){var n=window[i],s=!1;"object"==typeof n&&(s=!0,this.clear=function(){n[e]={}},this.setItem=function(i,s){void 0==s&&(s=i,i=t),n[e][i]=s},this.getItem=function(i){return void 0==i&&(i=t),void 0==n[e][i]?null:n[e][i]},this.removeItem=function(i){void 0==i&&(i=t),delete n[e][i]},this.getAllKeys=function(){return Object.keys(n[e])},void 0==n[e]&&this.clear()),this.isAvailable=function(){return s}},window.Persistence=new Persistence_sessionStorage,Persistence.isAvailable()||(window.Persistence=new Persistence_windowKey("py")),!Persistence.isAvailable()){var i=window.location.toString().indexOf("title"),n=window.location.toString().indexOf("main",i);i>0&&n>0&&n-i<10&&(window.Persistence=new Persistence_windowKey("qt"))}}</script>
<div id="performance">正确率:100%</div>
<hr />
<div class="text">{{edit:Question}}</div>
<ol class="options" id="optionList"></ol>
<hr />
<div id="key" class="text"><span class="small_text">上面的选项以<span class="green">此种形式</span>显示的为被你选中的正确选项,以<span class="blue">此种形式</span>显示的为未被你选中的正确选项,以<span class="wrong">此种形式</span>显示的不是正确选项却被你选中了。本题结果如下:</sapn><br/></div>
<hr>
<div class="extra">
<sapn class="imp">【解析】</sapn><br>{{edit:Extra}}</div>
<div id='styleButtons'>
<button id="blue_bold">蓝色加粗</button>
<button id="red_bold">红色加粗</button>
<button id="green_bold">绿色加粗</button>
<button id="orange_bold">橙色加粗</button>
</div>
<script>
/*以下对在复习界面中选择的文本的显示样式进行处理*/
var styleButtonDiv = document.getElementById('styleButtons');
var styleButtons = styleButtonDiv.getElementsByTagName('button');
for(i=0;i<styleButtons.length;i++){
styleButtons[i].onclick=function(ev){
var selection = document.getSelection();
var selectStr = selection.toString();
if (selectStr.trim() != '') {
var rang = selection.getRangeAt(0);
//卡片样式如果用u,b,i,a等标签名定义,只需声明tmpEle
var tmpEle
//卡片样式如果用.red之类的类名定义,可创建span元素
//var tmpEle = document.createElement('span');
var eleId=ev.target.getAttribute('id')//获取触发事件的元素的id
if(eleId=='blue_bold'){//如果是蓝色加粗按钮
//用标签名定义样式,需创建相应样式的元素
tmpEle = document.createElement('b');
//用类名定义样式,为前面创建的span元素指定类名
//tmpEle.className = 'blue';
}else if(eleId=='red_bold'){//如果是红色加粗按钮
//用标签名定义样式,需创建相应样式的元素
tmpEle = document.createElement('u');
//用类名定义样式,为前面创建的span元素指定类名
//tmpEle.className = 'red';
}else if(eleId=='green_bold'){//如果是绿色加粗按钮
//用标签名定义样式,需创建相应样式的元素
tmpEle = document.createElement('i');
//用类名定义样式,为前面创建的span元素指定类名
//tmpEle.className = 'green';
}else{//如果是橙色加粗按钮
//用标签名定义样式,需创建相应样式的元素
tmpEle = document.createElement('a');
//用类名定义样式,为前面创建的span元素指定类名
//tmpEle.className = 'orange';
}
//将选中的文本插入临时元素内部
rang.surroundContents(tmpEle);
rang.deleteContents(); // 删除选中内容
rang.insertNode(tmpEle); //插入临时元素替换内容
} else {
alert('请先选择文本!');
}
}
}
/*以下处理对做题结果的判断和输出*/
var myinfo;
myinfo = Persistence.getItem();
//计算成绩的函数
function calcScore() {
if (myinfo.choiced.length == 0) {
myinfo.ifright = "为什么一个都不选?"
myinfo.score = 0;
} else {
myinfo.score = 1;
for (var i = 0; i < myinfo.choiced.length; i++) {
if (myinfo.newOrderAnswer.indexOf(myinfo.choiced.charAt(i)) == -1) {
myinfo.score = 0;
myinfo.ifright = "错误";
break;
}
}
if (myinfo.score != 0) {
if (myinfo.newOrderAnswer.length == 1) {
myinfo.singleCorrect++;
myinfo.score = 1;
myinfo.ifright = "完全正确";
} else {
if (myinfo.choiced.length == myinfo.newOrderAnswer.length) {
myinfo.multiCorrect++;
myinfo.multiScore += 2;
myinfo.score = 2;
myinfo.choiced = myinfo.newOrderAnswer;
myinfo.ifright = "完全正确";
} else {
myinfo.partCorrect++;
myinfo.score = myinfo.choiced.length * 0.5;
myinfo.multiScore += myinfo.score;
myinfo.ifright = "不完全正确";
}
}
}
}
myinfo.sum += myinfo.score;
Persistence.setItem(myinfo);
}
//显示选项
var optionOl = document.getElementById("optionList");
ops = myinfo.newOrderOps;
for (var i = 0; i < ops.length; i++) {
var ch = String.fromCharCode(65 + i);
list = document.createElement("li");
label = document.createElement("label");
label.innerHTML = ops[i];
var input = document.createElement("input");
//根据选择的答案是否有多个字符判断应选用多选框还是单选框
input.type = myinfo.choiced.length > 1?"checkbox":"radio";
list.appendChild(input);
list.appendChild(label);
optionOl.appendChild(list);
if (myinfo.newOrderAnswer.indexOf(ch) >= 0) {
if (myinfo.choiced.indexOf(ch) >= 0) {
list.className = 'green';
input.checked = 'checked';
} else {
list.className = 'blue';
}
} else {
if (myinfo.choiced.indexOf(ch) >= 0) {
list.className = 'wrong';
input.checked = 'checked';
} else {
list.className = 'unchoiced'
}
}
}
//显示成绩
calcScore();
var performance = document.getElementById("performance");
var key = document.getElementById("key");
var total = myinfo.single + myinfo.multi;
if (typeof(myinfo) != "undefined") {
var singlePer = myinfo.single == 0 ? "100.00" :
((myinfo.singleCorrect / myinfo.single) * 100).toFixed(2);
var multiErr = myinfo.multi - myinfo.multiCorrect - myinfo.partCorrect
var multiPer = myinfo.multi == 0 ? "100.00" :
((myinfo.multiScore / (myinfo.multi * 2)) * 100).toFixed(2);
var scorePer = ((myinfo.sum / myinfo.totalScore) * 100).toFixed(2)
performance.innerHTML = "本次练习<span class='imp'>" + total +
"</span>题---单选题<span class='imp'>" + myinfo.single +
"</span>题---多选题<span class='imp'>" + myinfo.multi +
"</span>题;<br>单选正确<span class='imp'>" + myinfo.singleCorrect +
"</span>题---单选正确率<span class='imp'>" + singlePer +
"%</span>;<br>多选正确<span class='imp'>" + myinfo.multiCorrect +
"</span>题---多选部分正确<span class='imp'>" + myinfo.partCorrect +
"</span>题---多选错误<span class='imp'>" + multiErr +
"</span>题---多选得分<span class='imp'>" + myinfo.multiScore +
"</span>分---多选得分率<span class='imp'>" + multiPer +
"%</span>;<br>累计得分:<span class='imp'>" + myinfo.sum +
"</span>分---已做题目满分<span class='imp'>" + myinfo.totalScore +
"</span>分---得分率<span class='imp'>" + scorePer + "%</span>";
key.innerHTML += "<div>正确答案:<span class='imp'>" + myinfo.newOrderAnswer +
";</span>你的答案:<span class='imp'>" + myinfo.choiced +
";</span>结果判定:<span class='imp'>" +
myinfo.ifright + "</span>;本题得分:<span class='imp'>" +
myinfo.score + "</span>。</div>";
}
//} else {}
</script>
样式:
.card { font-family: Cambria-modify,Aa虎头虎脑,哈天随性体,干就完事了简,微软雅黑; font-size:1.3em; text-align:left;
color: white; background-color:#000000;}
table{border-collapse:collapse; }
td{padding:5px;text-align:center;border:2px solid green;vertical-align: middle;}
td.left{text-align:left;}
td.red{
border-right: solid thick red;
}
span {display:inline-block;}
hr{border: none;
height: 5px;
background-color:yellow;}
p{text-indent:2em;}
div{margin:5px auto }
.text{color:#ff0;font-weight:bold;font-size:1.2em;}
.orange, .imp,a, a:visited,a:hover,a:link,a:active{color:#F90;font-weight:bold;}
u, .red{color:red;font-weight: bold;text-decoration:none;}
.unchoiced{ color: white;}
.choiced{font-weight: bold; color: #f00;background-color:green;}
.extra{ margin-top:15px; font-size:1.2em; color: #eeeebb; text-align:left;line-height:1.5em;}
.green,i{ font-weight: bold; font-style:normal;color: #0f0;}
.blue,b{ font-weight: bold; font-style:normal;color: #3cf;}
.wrong{ font-weight: bold; color: red;text-decoration:line-through;}
.options{ list-style:upper-latin;font-size:1.2em;}
.options *{ cursor:pointer;}
.options *:hover[class="options"]{ font-weight:bold;color: #f90;}
.options li{ margin-top:0.8em;}
/*下面两行样式定义决定是否显示选项前面的圆形或方形框,注释掉就会显示*/
.options input[type="radio"]{display:none;}
.options input[type="checkbox"]{display:none;}
#performance{ text-align:left; font-size:16px;}0
最后说明:由于本文的功能用到了Anki插件,因此不支持插件的安卓和iOS版的Anki不能应用本文的功能。本文在windows11及Anki2.1.54上完成。