summaryrefslogtreecommitdiffstats
path: root/lib/parse
diff options
context:
space:
mode:
Diffstat (limited to 'lib/parse')
-rw-r--r--lib/parse/lex.js54
-rw-r--r--lib/parse/page.js62
-rw-r--r--lib/parse/renderer.js27
3 files changed, 138 insertions, 5 deletions
diff --git a/lib/parse/lex.js b/lib/parse/lex.js
index cec6047..813dd33 100644
--- a/lib/parse/lex.js
+++ b/lib/parse/lex.js
@@ -17,21 +17,65 @@ function splitSections(nodes) {
}, []).concat([section]); // Add remaining nodes
}
-// What is the type of this section
-function sectionType(nodes, idx) {
+function isQuizNode(node) {
+ return (/^[(\[][ x][)\]]/).test(node.text || node);
+}
+
+function isExercise(nodes) {
var codeType = { type: 'code' };
// Number of code nodes in section
var len = _.filter(nodes, codeType).length;
- if(
+ return (
// Got 3 or 4 code blocks
(len === 3 || len === 4) &&
// Ensure all nodes are at the end
_.all(_.last(nodes, len), codeType)
- )
- {
+ );
+}
+
+function isQuiz(nodes) {
+ if (nodes.length < 3) {
+ return false;
+ }
+
+ // Support having a first paragraph block
+ // before our series of questions
+ var quizNodes = nodes.slice(nodes[0].type === 'paragraph' ? 1 : 0);
+
+ // No questions
+ if (!_.some(quizNodes, { type: 'blockquote_start' })) {
+ return false;
+ }
+
+ // Check if section has list of questions
+ // or table of questions
+ var listIdx = _.findIndex(quizNodes, { type: 'list_item_start' });
+ var tableIdx = _.findIndex(quizNodes, { type: 'table' });
+
+ if(
+ // List of questions
+ listIdx !== -1 && isQuizNode(quizNodes[listIdx + 1]) &&
+
+ // Table of questions
+ (
+ tableIdx !== -1 &&
+ _.every(quizNodes[tableIdx].cells[0].slice(1), isQuizNode)
+ )
+ ) {
+ return true;
+ }
+
+ return false;
+}
+
+// What is the type of this section
+function sectionType(nodes, idx) {
+ if(isExercise(nodes)) {
return 'exercise';
+ } else if(isQuiz(nodes)) {
+ return 'quiz';
}
return 'normal';
diff --git a/lib/parse/page.js b/lib/parse/page.js
index 3479c85..eb118e4 100644
--- a/lib/parse/page.js
+++ b/lib/parse/page.js
@@ -36,6 +36,14 @@ function render(section, _options) {
return marked.parser(section, options);
}
+function quizQuestion(node) {
+ if (node.text) {
+ node.text = node.text.replace(/^([\[(])x([\])])/, "$1 $2");
+ } else {
+ return node.replace(/^([\[(])x([\])])/, "$1 $2");
+ }
+}
+
function parsePage(src, options) {
options = options || {};
@@ -76,6 +84,60 @@ function parsePage(src, options) {
context: codeNodes[3] ? codeNodes[3].text : null,
}
};
+ } else if (section.type === 'quiz') {
+ var quiz = [], question, foundFeedback = false;
+ var nonQuizNodes = section[0].type === 'paragraph' && section[1].type !== 'list_start' ? [section[0]] : [];
+ var quizNodes = section.slice(0);
+ quizNodes.splice(0, nonQuizNodes.length);
+
+ for (var i = 0; i < quizNodes.length; i++) {
+ var node = quizNodes[i];
+
+ if (question && (((node.type === 'list_end' || node.type === 'blockquote_end') && i === quizNodes.length - 1)
+ || node.type === 'table' || (node.type === 'paragraph' && !foundFeedback))) {
+ quiz.push({
+ base: render(question.questionNodes),
+ solution: render(question.solutionNodes),
+ feedback: render(question.feedbackNodes)
+ });
+ }
+
+ if (node.type === 'table' || (node.type === 'paragraph' && !foundFeedback)) {
+ question = { questionNodes: [], solutionNodes: [], feedbackNodes: [] };
+ }
+
+ if (node.type === 'blockquote_start') {
+ foundFeedback = true;
+ } else if (node.type === 'blockquote_end') {
+ foundFeedback = false;
+ }
+
+ if (node.type === 'table') {
+ question.solutionNodes.push(_.cloneDeep(node));
+ node.cells = node.cells.map(function(row) {
+ return row.map(quizQuestion);
+ });
+ question.questionNodes.push(node);
+ } else if (!/blockquote/.test(node.type)) {
+ if (foundFeedback) {
+ question.feedbackNodes.push(node);
+ } else if (node.type === 'paragraph' || node.type === 'text'){
+ question.solutionNodes.push(_.cloneDeep(node));
+ quizQuestion(node);
+ question.questionNodes.push(node);
+ } else {
+ question.solutionNodes.push(node);
+ question.questionNodes.push(node);
+ }
+ }
+ }
+
+ return {
+ id: section.id,
+ type: section.type,
+ content: render(nonQuizNodes),
+ quiz: quiz
+ };
}
// Render normal pages
diff --git a/lib/parse/renderer.js b/lib/parse/renderer.js
index 6b45a22..2a72d48 100644
--- a/lib/parse/renderer.js
+++ b/lib/parse/renderer.js
@@ -5,6 +5,7 @@ var path = require('path');
var marked = require('marked');
+var rendererId = 0;
function GitBookRenderer(options, extra_options) {
if(!(this instanceof GitBookRenderer)) {
@@ -13,6 +14,8 @@ function GitBookRenderer(options, extra_options) {
GitBookRenderer.super_.call(this, options);
this._extra_options = extra_options;
+ this.quizRowId = 0;
+ this.id = rendererId++;
}
inherits(GitBookRenderer, marked.Renderer);
@@ -83,6 +86,30 @@ GitBookRenderer.prototype.image = function(href, title, text) {
return GitBookRenderer.super_.prototype.image.call(this, _href, title, text);
};
+GitBookRenderer.prototype.tablerow = function(content) {
+ this.quizRowId += 1;
+ return GitBookRenderer.super_.prototype.tablerow(content);
+};
+
+var fieldRegex = /^([(\[])([ x])[\])]/;
+GitBookRenderer.prototype._createCheckboxAndRadios = function(text) {
+ var match = fieldRegex.exec(text);
+ if (!match) {
+ return text;
+ }
+ var field = "<input name='quiz-row-" + this.id + "-" + this.quizRowId + "' type='";
+ field += match[1] === '(' ? "radio" : "checkbox";
+ field += match[2] === 'x' ? "' checked/>" : "'/>";
+ return text.replace(fieldRegex, field);
+}
+
+GitBookRenderer.prototype.tablecell = function(content, flags) {
+ return GitBookRenderer.super_.prototype.tablecell(this._createCheckboxAndRadios(content), flags);
+};
+
+GitBookRenderer.prototype.listitem = function(text) {
+ return GitBookRenderer.super_.prototype.listitem(this._createCheckboxAndRadios(text));
+};
// Exports
module.exports = GitBookRenderer;