From ffb0b1201268134ca697723b473ec74c010442d3 Mon Sep 17 00:00:00 2001 From: Aaron O'Mullan Date: Mon, 13 Oct 2014 14:17:08 +0200 Subject: [PATCH 1/4] Add unit test for quiz false positivies --- test/fixtures/FALSE_QUIZ.md | 120 ++++++++++++++++++++++++++++++++++++ test/page.js | 8 +++ 2 files changed, 128 insertions(+) create mode 100644 test/fixtures/FALSE_QUIZ.md diff --git a/test/fixtures/FALSE_QUIZ.md b/test/fixtures/FALSE_QUIZ.md new file mode 100644 index 000000000..46f9a004c --- /dev/null +++ b/test/fixtures/FALSE_QUIZ.md @@ -0,0 +1,120 @@ +# Learn Node.js by Example + + +## Requirements + +- [x] A computer with internet access +- [ ] Time: 30h (e.g. 2 months 30 mins per day or **1 week intensive**) + + +## What is Node.js ? + +Node.js lets you *easily* build networked software (websites, applications "apps", +using JavaScript). + +Its not "*point-and-click*" like WordPress, SquareSpace or Salesforce; +you will need to write some "code". But as I will demonstrate, that's +a *lot* easier than it sounds and gives you more power/flexibility +and puts you in full control. + +## Node.js is not "Version 1.0" yet can I used it in Production? + +Yes! Some of the biggest organisations/companies in the world +are using Node.js in Production systems: + +[Alibaba](https://github.com/alibaba/node-hbase-client), +[Ajax.org](Ajax.org), +[Box.com](http://tech.blog.box.com/2014/06/node-js-high-availability-at-box/), British Sky Broadcasting (Sky/Now TV), +CNN, +[Cloudup](https://cloudup.com/), +Conde Nast, +[DirectTV](http://strongloop.com/strongblog/node-summit-media-companies-embrace-node-js-for-rapidly-developing-responsive-apps/), +[Dow Jones](http://nodejs.org/industry), +eBay, +[FeedHenry](http://www.feedhenry.com/), +[GitHub](https://twitter.com/github/status/16979699217465344), +[Google](http://venturebeat.com/2012/01/24/node-at-google-mozilla-yahoo/), +[Groupon](http://nodeup.com/fiftyeight), +HBO, +Help.com, +[HP](https://github.com/joyent/node/wiki/Projects,-Applications,-and-Companies-Using-Node), +iTV, +[Joyent](https://www.joyent.com/) (duh!), +[Klout](https://klout.com), +LinkedIn, +McDonalds, +[Medium](https://medium.com/the-story), +Mozilla, +NetFlix, +[OpenTable](http://hapijs.com/community), +PayPal, +Pearson, +~~Q~~, +[Revolt](http://revolt.tv/), +[Square](https://modulus.io/companies-using-node), +Tesco, +ThomasCook, +Trello, +Uber, +Voxer, +Walmart, +Wikimedia (in progress of moving to SOA with node!) +Yahoo, +Yammer, +[Yandex](https://www.youtube.com/watch?v=zdCxgdH4wZo), +[Zendesk](http://radar.zendesk.com/) + +Want more? See: http://nodejs.org/industry/ and
+https://github.com/joyent/node/wiki/Projects,-Applications,-and-Companies-Using-Node + + +# Try it! + +## Download & Install + +> http://nodejs.org/download/ + + +## Node.js (Core) API + +The node.js ("core") has many useful modules. + +Bookmark: [http://nodejs.org/api](http://nodejs.org/api/) (you will come back to it) + + + +## Stability (Can we use it?) + +> *Which node.js* ***core*** *package(s) can/should I use?* + +Every core module has a +["***Stability Index***"](http://nodejs.org/api/documentation.html#documentation_stability_index) +rating on the node.js API. + +**General rule**: If you are being *paid* to write code +that runs in node.js,
pick core modules/methods +with stability **Stable**, **API Frozen** and **Locked**. + +![Node.js Stability Index](http://i.imgur.com/xIroFrS.png) + + +### Examples + +- [**cluster**](http://nodejs.org/api/cluster.html) is ***Experimental*** - don't use +- [**domain**](http://nodejs.org/api/domain.html) is ***Unstable*** - don't use +- [**path**](http://nodejs.org/api/path.html) is ***Stable*** - use +- [**events**](http://nodejs.org/api/events.html) is ***Frozen*** - use +- [**assert**](http://nodejs.org/api/assert.html) is ***Locked*** - use + +Core Modules to Learn + +- path +- os + + + +Community Modules to Learn: + +- [jscs](https://www.npmjs.org/package/jscs) - code style checker +- [q](https://www.npmjs.org/package/q) - promises library +- [nd](https://www.npmjs.org/package/nd) - view documentation for a module diff --git a/test/page.js b/test/page.js index 42243d857..2c597534b 100644 --- a/test/page.js +++ b/test/page.js @@ -122,3 +122,11 @@ describe('Relative images', function() { assert(LEXED[0].content.indexOf('"preview2.png"') !== -1); }); }); + +describe('Section parsing', function() { + it('should not have false positive quiz parsing', function() { + var LEXED = loadPage('FALSE_QUIZ'); + + assert.not.equal(LEXED[0].type, 'quiz'); + }); +}); From d730a3aac601f000d770dbccca02ac4bb2d07245 Mon Sep 17 00:00:00 2001 From: Aaron O'Mullan Date: Mon, 13 Oct 2014 15:07:46 +0200 Subject: [PATCH 2/4] Split section classification to separate files Maybe have a sections folder in the future and one per type (quiz/exercise/normal) ? --- lib/parse/is_exercise.js | 17 ++++++++++++ lib/parse/is_quiz.js | 45 ++++++++++++++++++++++++++++++++ lib/parse/lex.js | 56 +++------------------------------------- 3 files changed, 65 insertions(+), 53 deletions(-) create mode 100644 lib/parse/is_exercise.js create mode 100644 lib/parse/is_quiz.js diff --git a/lib/parse/is_exercise.js b/lib/parse/is_exercise.js new file mode 100644 index 000000000..74ed75396 --- /dev/null +++ b/lib/parse/is_exercise.js @@ -0,0 +1,17 @@ +var _ = require('lodash'); + +function isExercise(nodes) { + var codeType = { type: 'code' }; + + // Number of code nodes in section + var len = _.filter(nodes, codeType).length; + + return ( + // Got 3 or 4 code blocks + (len === 3 || len === 4) && + // Ensure all nodes are at the end + _.all(_.last(nodes, len), codeType) + ); +} + +module.exports = isExercise; diff --git a/lib/parse/is_quiz.js b/lib/parse/is_quiz.js new file mode 100644 index 000000000..aa6125a87 --- /dev/null +++ b/lib/parse/is_quiz.js @@ -0,0 +1,45 @@ +var _ = require('lodash'); + +function isQuizNode(node) { + return (/^[(\[][ x][)\]]/).test(node.text || node); +} + +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 && + // Last entry + tableIdx === nodes.length - 1 && + _.every(quizNodes[tableIdx].cells[0].slice(1), isQuizNode) + ) + ) { + return true; + } + + return false; +} + +module.exports = isQuiz; diff --git a/lib/parse/lex.js b/lib/parse/lex.js index d9f9fd97d..3391acfde 100644 --- a/lib/parse/lex.js +++ b/lib/parse/lex.js @@ -1,6 +1,9 @@ var _ = require('lodash'); var kramed = require('kramed'); +var isExercise = require('./is_exercise'); +var isQuiz = require('./is_quiz'); + // Split a page up into sections (lesson, exercises, ...) function splitSections(nodes) { var section = []; @@ -17,59 +20,6 @@ function splitSections(nodes) { }, []).concat([section]); // Add remaining nodes } -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; - - 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)) { From 676b2287ee2595843daa18bee4effeb6f021b2ed Mon Sep 17 00:00:00 2001 From: Aaron O'Mullan Date: Mon, 13 Oct 2014 15:50:12 +0200 Subject: [PATCH 3/4] Rewrite quiz logic to be more robust Fixes #470 --- lib/parse/is_quiz.js | 101 +++++++++++++++++++++++++++++-------------- test/page.js | 2 +- 2 files changed, 70 insertions(+), 33 deletions(-) diff --git a/lib/parse/is_quiz.js b/lib/parse/is_quiz.js index aa6125a87..4a24cfcfb 100644 --- a/lib/parse/is_quiz.js +++ b/lib/parse/is_quiz.js @@ -4,42 +4,79 @@ function isQuizNode(node) { return (/^[(\[][ x][)\]]/).test(node.text || node); } +function isTableQuestion(nodes) { + var block = questionBlock(nodes); + return ( + block.length === 1 && + block[0].type === 'table' && + _.all(block[0].cells[0].slice(1), isQuizNode) + ); +} + +function isListQuestion(nodes) { + var block = questionBlock(nodes); + // Counter of when we go in and out of lists + var inlist = 0; + // Number of lists we found + var lists = 0; + // Elements found outside a list + var outsiders = 0; + // Ensure that we have nothing except lists + _.each(block, function(node) { + if(node.type === 'list_start') { + inlist++; + } else if(node.type === 'list_end') { + inlist--; + lists++; + } else if(inlist === 0) { + // Found non list_start or list_end whilst outside a list + outsiders++; + } + }); + return lists > 0 && outsiders === 0; +} + +function isQuestion(nodes) { + return isListQuestion(nodes) || isTableQuestion(nodes); +} + +// Remove (optional) paragraph header node and blockquote +function questionBlock(nodes) { + return nodes.slice( + nodes[0].type === 'paragraph' ? 1 : 0, + _.findIndex(nodes, { type: 'blockquote_start' }) + ); +} + +function splitQuestions(nodes) { + // Represents nodes in current question + var buffer = []; + return _.reduce(nodes, function(accu, node) { + // Add node to buffer + buffer.push(node); + + // Flush buffer once we hit the end of a question + if(node.type === 'blockquote_end') { + accu.push(buffer); + // Clear buffer + buffer = []; + } + + return accu; + }, []); +} + function isQuiz(nodes) { - if (nodes.length < 3) { + // Extract potential questions + var questions = splitQuestions(nodes); + + // Nothing that looks like questions + if(questions.length === 0) { 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 && - // Last entry - tableIdx === nodes.length - 1 && - _.every(quizNodes[tableIdx].cells[0].slice(1), isQuizNode) - ) - ) { - return true; - } - - return false; + // Ensure all questions are correctly structured + return _.all(questions, isQuestion); } module.exports = isQuiz; diff --git a/test/page.js b/test/page.js index 2c597534b..cd843c60a 100644 --- a/test/page.js +++ b/test/page.js @@ -127,6 +127,6 @@ describe('Section parsing', function() { it('should not have false positive quiz parsing', function() { var LEXED = loadPage('FALSE_QUIZ'); - assert.not.equal(LEXED[0].type, 'quiz'); + assert.equal(LEXED[0].type, 'normal'); }); }); From 84d7662fd8a2d1abfd0feb92f54064e5d9b9d072 Mon Sep 17 00:00:00 2001 From: Aaron O'Mullan Date: Mon, 13 Oct 2014 15:55:59 +0200 Subject: [PATCH 4/4] Allow quiz titles in validation --- lib/parse/is_quiz.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/parse/is_quiz.js b/lib/parse/is_quiz.js index 4a24cfcfb..3322ff0c0 100644 --- a/lib/parse/is_quiz.js +++ b/lib/parse/is_quiz.js @@ -68,7 +68,12 @@ function splitQuestions(nodes) { function isQuiz(nodes) { // Extract potential questions - var questions = splitQuestions(nodes); + var questions = splitQuestions( + // Skip quiz title if there + nodes.slice( + (nodes[0] && nodes[0].type) === 'paragraph' ? 1 : 0 + ) + ); // Nothing that looks like questions if(questions.length === 0) {