Skip to content

Commit b8726a3

Browse files
committed
Partial/draft quiz functionality implementation.
#2.
1 parent 1f02587 commit b8726a3

File tree

6 files changed

+271
-2
lines changed

6 files changed

+271
-2
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ cache
22
output
33
.npm
44
node_modules
5-
package.json
65
package-lock.json
76
.DS_Store
7+
*.png.webp

build-local-development.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ set -e
44

55
env=dev
66

7+
docker run -u $(id -u) -v $PWD:/antora:Z --rm -t -e HOME=/antora antora/antora:3.1.5 npm i
8+
79
# Only generating English
810
for lang in en; do
911
echo "------- Generating $lang --------"
1012
rm -Rf output/$lang
11-
docker run -u $(id -u) -e CI=true -v $PWD:/antora:Z --rm -t antora/antora:3.1.2 development-$lang-playbook.yml
13+
docker run -u $(id -u) -e CI=true -v $PWD:/antora:Z --rm -t antora/antora:3.1.5 development-$lang-playbook.yml
1214
done
1315
echo

development-en-playbook.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ ui:
3939
snapshot: true
4040
supplemental_files: ./supplemental_ui
4141
asciidoc:
42+
extensions:
43+
- ./lib/asciidoctor-question
4244
attributes:
4345
idprefix: ''
4446
idseparator: '-'

lib/asciidoctor-question.js

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
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+
}

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"dependencies": {
3+
"@antora/lunr-extension": "^1.0.0-alpha.8",
4+
"cheerio": "^1.0.0-rc.12"
5+
}
6+
}

supplemental_ui/css/training.css

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,39 @@
137137
background: radial-gradient(circle, #bec2bc, #bec2bc 66%, transparent 66%);
138138
content: url(../img/icons/workspace.png);
139139
}
140+
141+
/* For the question/quiz extension */
142+
143+
.hidden {
144+
display: none;
145+
}
146+
147+
question[data-type=gap] gap > input.incorrect {
148+
font-weight: bold;
149+
color: red;
150+
}
151+
152+
question[data-type=gap] gap > answer, div[id*=question][data-type=gap] gap > input.correct {
153+
font-weight: bold;
154+
color: green;
155+
}
156+
157+
question[data-type=mc] input[type="checkbox"].show ~ span::before {
158+
display: inline;
159+
width: 16px;
160+
height: 16px;
161+
}
162+
163+
question[data-type=mc] input[type="checkbox"][data-correct="true"].show ~ span::before {
164+
content: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgaGVpZ2h0PSIxNnB4IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjE2cHgiIHhtbDpzcGFjZT0icHJlc2VydmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxwYXRoIGZpbGw9Im9saXZlZHJhYiIgc3Ryb2tlPSJvbGl2ZWRyYWIiIGQ9Ik0yMS42NTIsMy4yMTFjLTAuMjkzLTAuMjk1LTAuNzctMC4yOTUtMS4wNjEsMEw5LjQxLDE0LjM0ICBjLTAuMjkzLDAuMjk3LTAuNzcxLDAuMjk3LTEuMDYyLDBMMy40NDksOS4zNTFDMy4zMDQsOS4yMDMsMy4xMTQsOS4xMywyLjkyMyw5LjEyOUMyLjczLDkuMTI4LDIuNTM0LDkuMjAxLDIuMzg3LDkuMzUxICBsLTIuMTY1LDEuOTQ2QzAuMDc4LDExLjQ0NSwwLDExLjYzLDAsMTEuODIzYzAsMC4xOTQsMC4wNzgsMC4zOTcsMC4yMjMsMC41NDRsNC45NCw1LjE4NGMwLjI5MiwwLjI5NiwwLjc3MSwwLjc3NiwxLjA2MiwxLjA3ICBsMi4xMjQsMi4xNDFjMC4yOTIsMC4yOTMsMC43NjksMC4yOTMsMS4wNjIsMGwxNC4zNjYtMTQuMzRjMC4yOTMtMC4yOTQsMC4yOTMtMC43NzcsMC0xLjA3MUwyMS42NTIsMy4yMTF6Ii8+PC9zdmc+);
165+
}
166+
167+
question[data-type=mc] input[type="checkbox"][data-correct="false"].show ~ span::before {
168+
content: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgaGVpZ2h0PSIxNnB4IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjE2cHgiIHhtbDpzcGFjZT0icHJlc2VydmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxwYXRoIGZpbGw9Im1hcm9vbiIgc3Ryb2tlPSJtYXJvb24iIGQ9Ik0yMi4yNDUsNC4wMTVjMC4zMTMsMC4zMTMsMC4zMTMsMC44MjYsMCwxLjEzOWwtNi4yNzYsNi4yN2MtMC4zMTMsMC4zMTItMC4zMTMsMC44MjYsMCwxLjE0bDYuMjczLDYuMjcyICBjMC4zMTMsMC4zMTMsMC4zMTMsMC44MjYsMCwxLjE0bC0yLjI4NSwyLjI3N2MtMC4zMTQsMC4zMTItMC44MjgsMC4zMTItMS4xNDIsMGwtNi4yNzEtNi4yNzFjLTAuMzEzLTAuMzEzLTAuODI4LTAuMzEzLTEuMTQxLDAgIGwtNi4yNzYsNi4yNjdjLTAuMzEzLDAuMzEzLTAuODI4LDAuMzEzLTEuMTQxLDBsLTIuMjgyLTIuMjhjLTAuMzEzLTAuMzEzLTAuMzEzLTAuODI2LDAtMS4xNGw2LjI3OC02LjI2OSAgYzAuMzEzLTAuMzEyLDAuMzEzLTAuODI2LDAtMS4xNEwxLjcwOSw1LjE0N2MtMC4zMTQtMC4zMTMtMC4zMTQtMC44MjcsMC0xLjE0bDIuMjg0LTIuMjc4QzQuMzA4LDEuNDE3LDQuODIxLDEuNDE3LDUuMTM1LDEuNzMgIEwxMS40MDUsOGMwLjMxNCwwLjMxNCwwLjgyOCwwLjMxNCwxLjE0MSwwLjAwMWw2LjI3Ni02LjI2N2MwLjMxMi0wLjMxMiwwLjgyNi0wLjMxMiwxLjE0MSwwTDIyLjI0NSw0LjAxNXoiLz48L3N2Zz4=);
169+
}
170+
171+
/* Matt's hack because the question is duplicated */
172+
173+
question[data-type=mc] div.content div:nth-child(2 of .ulist) {
174+
display: none;
175+
}

0 commit comments

Comments
 (0)