|
| 1 | +// This is a partial port of the Asciidoctor Question extension to Asciidoctor.js. |
| 2 | +// |
| 3 | +// Original Ruby extension: https://github.com/vifito/asciidoctor-question |
| 4 | +// |
| 5 | +// Only the 'mc' (multiple choice) question type is implemented, and only the answer part. |
| 6 | +// (as this is the way the extension has been used). |
| 7 | +// |
| 8 | + |
| 9 | +'use strict' |
| 10 | + |
| 11 | +const cheerio = require('cheerio') |
| 12 | + |
| 13 | +const q_regex = /^-\s?\[/ |
| 14 | + |
| 15 | +var question_id = 0 |
| 16 | + |
| 17 | +module.exports = function (registry) { |
| 18 | + // A block processor for [question] blocks |
| 19 | + // Splits questions from answers, adds the 'interactive' role |
| 20 | + // and adds the resolve/reset buttons. |
| 21 | + registry.block(function () { |
| 22 | + var self = this |
| 23 | + self.named('question') |
| 24 | + self.onContext('literal') |
| 25 | + self.process(function (parent, source, tag, reader) { |
| 26 | + // TODO: Work out type, mc or not. |
| 27 | + |
| 28 | + // Generate an id |
| 29 | + //console.log("tag", tag) |
| 30 | + var id = question_id++ |
| 31 | + //console.log("id", id) |
| 32 | + |
| 33 | + var question = [] |
| 34 | + var answers = ['[options=interactive]'] |
| 35 | + |
| 36 | + var lines = source.getLines().map(function (l) { |
| 37 | + if (q_regex.test(l)) { |
| 38 | + l = l.replace(']', '] +++ <span></span> +++') |
| 39 | + answers.push(l) |
| 40 | + //console.log("A", l) |
| 41 | + } else { |
| 42 | + question.push(l) |
| 43 | + //console.log("Q", l) |
| 44 | + } |
| 45 | + }) |
| 46 | + |
| 47 | + // Attributes become question_{id}_type=mc |
| 48 | + var attrs = {'id': "question_"+id+"_type=mc"} |
| 49 | + |
| 50 | + // TODO: shuffle answers if needed |
| 51 | + // attrs['id'] += '_shuffle' unless tag[:shuffle].nil? |
| 52 | + |
| 53 | + // new block with the attributes |
| 54 | + //console.log(self.createBlock.toString()) |
| 55 | + var new_parent = self.createBlock(parent, 'open', '', attrs) |
| 56 | + //console.log("n_p", new_parent) |
| 57 | + |
| 58 | + // TODO: process questions |
| 59 | + |
| 60 | + // process answers |
| 61 | + const PreprocessorReader = global.Opal.Asciidoctor.PreprocessorReader |
| 62 | + const Reader = global.Opal.Asciidoctor.Reader |
| 63 | + var answer_reader = Reader.$new(answers, null, null) |
| 64 | + //console.log("a_r", answer_reader) |
| 65 | + //console.log("answer_reader.pi", answerreader.pushInclude.toString()) |
| 66 | + |
| 67 | + const Parser = global.Opal.Asciidoctor.Parser |
| 68 | + //console.log("p", Parser) |
| 69 | + //console.log("pnb", Parser.$next_block) |
| 70 | + //console.log("pnb", Parser.$next_block.toString()) |
| 71 | + |
| 72 | + var answers_block = Parser.$next_block(answer_reader, new_parent) |
| 73 | + //console.log("ans_bl", answers_block) |
| 74 | + |
| 75 | + // answers id answers_mc_{id} |
| 76 | + |
| 77 | + // each answer id answer_md_{id}_{aid} |
| 78 | + |
| 79 | + //console.log(new_parent) |
| 80 | + |
| 81 | + //self.createBlock(parent, 'pass', lines[0]) |
| 82 | + //console.log("n_p.gB", new_parent.getBlocks().toString()) |
| 83 | + new_parent.getBlocks().push(answers_block) |
| 84 | + |
| 85 | + new_parent.getBlocks().push(self.createBlock(parent, 'pass', "<p style=\"margin-bottom: 25px\"><button onclick='resolve("+id+")'>resolve</button><button onclick='reset("+id+")'>reset</button></p>")) |
| 86 | + |
| 87 | + //console.log('-') |
| 88 | + // TODO: For some reason, the unaltered lines are then processed again. Maybe I'm not |
| 89 | + // consuming them properly. |
| 90 | + return new_parent |
| 91 | + }) |
| 92 | + }) |
| 93 | + |
| 94 | + registry.postprocessor(function () { |
| 95 | + var self = this |
| 96 | + self.process(function (doc, output) { |
| 97 | + // TODO Add CSS (although its in the theme) |
| 98 | + |
| 99 | + const $ = cheerio.load(output, null, false) |
| 100 | + |
| 101 | + // Generate data- properties for the question elements. |
| 102 | + $('div[id*=question]').each(function (i, elm) { |
| 103 | + var qq = $('<question>') |
| 104 | + |
| 105 | + //console.log("i", i) |
| 106 | + //console.log("elm", $(elm).text()) |
| 107 | + //console.log("end elm") |
| 108 | + var question_id = $(elm).attr('id') |
| 109 | + //console.log("orig id", question_id) |
| 110 | + var question_parts = question_id.split('_') |
| 111 | + //console.log(question_parts) |
| 112 | + var question_newid = question_parts.shift() + '_' + question_parts.shift() |
| 113 | + //var attribs = {"id": question_newid} |
| 114 | + qq.attr('id', question_newid) |
| 115 | + question_parts.forEach((data) => { |
| 116 | + var tmp = data.split('=') |
| 117 | + //console.log('data-'+tmp[0]+'='+tmp[1]) |
| 118 | + //question_parts['data-'+tmp[0]] = tmp[1] |
| 119 | + qq.attr('data-'+tmp[0], tmp[1]) |
| 120 | + }) |
| 121 | + |
| 122 | + //console.log(`<question id="${question_newid}"> ... </question>`) |
| 123 | + |
| 124 | + qq.html($(elm).html()) |
| 125 | + //console.log(qq.html()) |
| 126 | + |
| 127 | + $(elm).replaceWith(qq) |
| 128 | + }) |
| 129 | + |
| 130 | + $('question[id*=question][data-type=mc] input[type="checkbox"]').each(function (i, elm) { |
| 131 | + //console.log("AOEU", $(elm).html()) |
| 132 | + $(elm).attr('data-correct', $(elm).attr('checked') != null) |
| 133 | + $(elm).attr('checked', null) |
| 134 | + }) |
| 135 | + |
| 136 | + var javascript = ` |
| 137 | +<script type="text/javascript"> |
| 138 | +function onLoad() { |
| 139 | + var questions = document.getElementsByTagName("question") |
| 140 | + for (var pos = 0; pos < questions.length; pos++) { |
| 141 | + var question = questions[pos] |
| 142 | + if (question.getAttribute('data-type') == 'mc') { |
| 143 | + if (question.getAttribute('data-shuffle') != null) shuffleAnswers(question) |
| 144 | + } |
| 145 | + } |
| 146 | +} |
| 147 | +
|
| 148 | +function shuffleAnswers(question) { |
| 149 | + var answers = null |
| 150 | + var content = question.children[0] |
| 151 | + for (var i = content.children.length-1; i >= 0; i--) { |
| 152 | + if(content.children[i].className.indexOf('checklist') > -1) { |
| 153 | + answers = content.children[i].children[0] |
| 154 | + break; |
| 155 | + } |
| 156 | + } |
| 157 | + if(answers == null) return |
| 158 | +
|
| 159 | + for (var i = answers.children.length; i >= 0; i--) { |
| 160 | + answers.appendChild(answers.children[Math.random() * i | 0]); |
| 161 | + } |
| 162 | +} |
| 163 | +
|
| 164 | +function resolve(questionId) { |
| 165 | + var q = document.getElementById("question_" + questionId) |
| 166 | + var type = q.getAttribute("data-type") |
| 167 | + var elems |
| 168 | + var pos |
| 169 | + var answer |
| 170 | +
|
| 171 | + if(type == "mc") { |
| 172 | + elems = q.getElementsByTagName("input") |
| 173 | + for(pos = 0; pos < elems.length; pos++) { |
| 174 | + answer = elems[pos] |
| 175 | + answer.setAttribute("class", "show") |
| 176 | + } |
| 177 | + } else if(type == "gap") { |
| 178 | + elems = q.getElementsByTagName("gap") |
| 179 | + for(pos = 0; pos < elems.length; pos++) { |
| 180 | + var gap = elems[pos] |
| 181 | + var input = gap.getElementsByTagName("input")[0] |
| 182 | + answer = gap.getElementsByTagName("answer")[0] |
| 183 | + if(input.value == answer.textContent) { |
| 184 | + input.setAttribute("class", "correct") |
| 185 | + } else { |
| 186 | + input.setAttribute("class", "incorrect") |
| 187 | + answer.setAttribute("class", "") |
| 188 | + } |
| 189 | + } |
| 190 | + } |
| 191 | +} |
| 192 | +
|
| 193 | +function reset(questionId) { |
| 194 | + var q = document.getElementById("question_" + questionId) |
| 195 | + var type = q.getAttribute("data-type") |
| 196 | + var elems |
| 197 | + var pos |
| 198 | + var answer |
| 199 | +
|
| 200 | + if(type == "mc") { |
| 201 | + elems = q.getElementsByTagName("input") |
| 202 | + for(pos = 0; pos < elems.length; pos++) { |
| 203 | + answer = elems[pos] |
| 204 | + answer.setAttribute("class", "") |
| 205 | + answer.checked = false |
| 206 | + } |
| 207 | + } else if(type == "gap") { |
| 208 | + elems = q.getElementsByTagName("gap") |
| 209 | + for(pos = 0; pos < elems.length; pos++) { |
| 210 | + var gap = elems[pos] |
| 211 | + var input = gap.getElementsByTagName("input")[0] |
| 212 | + answer = gap.getElementsByTagName("answer")[0] |
| 213 | + input.setAttribute("class", "") |
| 214 | + answer.setAttribute("class", "hidden") |
| 215 | + } |
| 216 | + } |
| 217 | +} |
| 218 | +</script>` |
| 219 | + |
| 220 | + return $.html() + javascript |
| 221 | + }) |
| 222 | + }) |
| 223 | +} |
0 commit comments