feat: add a markdown parser for challenges

This commit is contained in:
Mrugesh Mohapatra 2018-09-27 16:00:11 +05:30
parent 77d057d4e5
commit f022177352
15 changed files with 8620 additions and 0 deletions

View File

@ -0,0 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`frontmatter-to-data plugin should have an output to match the snapshot 1`] = `
Object {
"challengeType": 0,
"id": "bd7123c8c441eddfaeb5bdef",
"title": "Say Hello to HTML Elements",
"videoUrl": "https://scrimba.com/p/pVMPUv/cE8Gpt2",
}
`;

View File

@ -0,0 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`tests-to-data plugin should have an output to match the snapshot 1`] = `
Object {
"tests": Array [
Object {
"testString": "assert.isTrue((/hello(\\\\s)+world/gi).test($('h1').text()), 'Your <code>h1</code> element should have the text \\"Hello World\\".');",
"text": "Your <code>h1</code> element should have the text \\"Hello World\\".",
},
],
}
`;

View File

@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`text-to-data should have an output to match the snapshot 1`] = `
Object {
"description": "<section id=\\"description\\">
<p>Welcome to freeCodeCamp's HTML coding challenges. These will walk you through web development step-by-step.</p>
<p>Lorem Ipsum with <code>some code</code></p>
<blockquote>
<p>Some text in a blockquote</p>
<p>Some text in a blockquote, with <code>code</code></p>
</blockquote>
<pre><code class=\\"language-html\\">&#x3C;p>We aim to preserve this&#x3C;/p>
</code></pre>
</section>",
"instructions": "<section id=\\"instructions\\">
<p>To pass the test on this challenge, change your <code>h1</code> element's text to say \\"Hello World\\".</p>
</section>",
}
`;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,915 @@
{
"type": "root",
"children": [
{
"type": "yaml",
"value":
"id: bd7123c8c441eddfaeb5bdef\ntitle: Say Hello to HTML Elements\nchallengeType: 0\nvideoUrl: https://scrimba.com/p/pVMPUv/cE8Gpt2",
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 6,
"column": 4,
"offset": 134
},
"indent": [1, 1, 1, 1, 1]
}
},
{
"type": "heading",
"depth": 2,
"children": [
{
"type": "text",
"value": "Description",
"position": {
"start": {
"line": 8,
"column": 4,
"offset": 139
},
"end": {
"line": 8,
"column": 15,
"offset": 150
},
"indent": []
}
}
],
"position": {
"start": {
"line": 8,
"column": 1,
"offset": 136
},
"end": {
"line": 8,
"column": 15,
"offset": 150
},
"indent": []
}
},
{
"type": "html",
"value": "<section id='description'>",
"position": {
"start": {
"line": 9,
"column": 1,
"offset": 151
},
"end": {
"line": 9,
"column": 27,
"offset": 177
},
"indent": []
}
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value":
"Welcome to freeCodeCamp's HTML coding challenges. These will walk you through web development step-by-step.",
"position": {
"start": {
"line": 11,
"column": 1,
"offset": 179
},
"end": {
"line": 11,
"column": 108,
"offset": 286
},
"indent": []
}
}
],
"position": {
"start": {
"line": 11,
"column": 1,
"offset": 179
},
"end": {
"line": 11,
"column": 108,
"offset": 286
},
"indent": []
}
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "Lorem Ipsum with ",
"position": {
"start": {
"line": 13,
"column": 1,
"offset": 288
},
"end": {
"line": 13,
"column": 18,
"offset": 305
},
"indent": []
}
},
{
"type": "inlineCode",
"value": "some code",
"position": {
"start": {
"line": 13,
"column": 18,
"offset": 305
},
"end": {
"line": 13,
"column": 29,
"offset": 316
},
"indent": []
}
}
],
"position": {
"start": {
"line": 13,
"column": 1,
"offset": 288
},
"end": {
"line": 13,
"column": 29,
"offset": 316
},
"indent": []
}
},
{
"type": "blockquote",
"children": [
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "Some text in a blockquote",
"position": {
"start": {
"line": 15,
"column": 3,
"offset": 320
},
"end": {
"line": 15,
"column": 28,
"offset": 345
},
"indent": []
}
}
],
"position": {
"start": {
"line": 15,
"column": 3,
"offset": 320
},
"end": {
"line": 15,
"column": 28,
"offset": 345
},
"indent": []
}
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "Some text in a blockquote, with ",
"position": {
"start": {
"line": 17,
"column": 3,
"offset": 349
},
"end": {
"line": 17,
"column": 35,
"offset": 381
},
"indent": []
}
},
{
"type": "inlineCode",
"value": "code",
"position": {
"start": {
"line": 17,
"column": 35,
"offset": 381
},
"end": {
"line": 17,
"column": 41,
"offset": 387
},
"indent": []
}
}
],
"position": {
"start": {
"line": 17,
"column": 3,
"offset": 349
},
"end": {
"line": 17,
"column": 41,
"offset": 387
},
"indent": []
}
}
],
"position": {
"start": {
"line": 15,
"column": 1,
"offset": 318
},
"end": {
"line": 17,
"column": 41,
"offset": 387
},
"indent": [1, 1]
}
},
{
"type": "code",
"lang": "html",
"value": "<p>We aim to preserve this</p>",
"position": {
"start": {
"line": 19,
"column": 1,
"offset": 389
},
"end": {
"line": 21,
"column": 4,
"offset": 431
},
"indent": [1, 1]
}
},
{
"type": "html",
"value": "</section>",
"position": {
"start": {
"line": 22,
"column": 1,
"offset": 432
},
"end": {
"line": 22,
"column": 11,
"offset": 442
},
"indent": []
}
},
{
"type": "heading",
"depth": 2,
"children": [
{
"type": "text",
"value": "Instructions",
"position": {
"start": {
"line": 24,
"column": 4,
"offset": 447
},
"end": {
"line": 24,
"column": 16,
"offset": 459
},
"indent": []
}
}
],
"position": {
"start": {
"line": 24,
"column": 1,
"offset": 444
},
"end": {
"line": 24,
"column": 16,
"offset": 459
},
"indent": []
}
},
{
"type": "html",
"value": "<section id='instructions'>",
"position": {
"start": {
"line": 25,
"column": 1,
"offset": 460
},
"end": {
"line": 25,
"column": 28,
"offset": 487
},
"indent": []
}
},
{
"type": "paragraph",
"children": [
{
"type": "text",
"value": "To pass the test on this challenge, change your ",
"position": {
"start": {
"line": 27,
"column": 1,
"offset": 489
},
"end": {
"line": 27,
"column": 49,
"offset": 537
},
"indent": []
}
},
{
"type": "inlineCode",
"value": "h1",
"position": {
"start": {
"line": 27,
"column": 49,
"offset": 537
},
"end": {
"line": 27,
"column": 53,
"offset": 541
},
"indent": []
}
},
{
"type": "text",
"value": " element's text to say \"Hello World\".",
"position": {
"start": {
"line": 27,
"column": 53,
"offset": 541
},
"end": {
"line": 27,
"column": 90,
"offset": 578
},
"indent": []
}
}
],
"position": {
"start": {
"line": 27,
"column": 1,
"offset": 489
},
"end": {
"line": 27,
"column": 90,
"offset": 578
},
"indent": []
}
},
{
"type": "html",
"value": "</section>",
"position": {
"start": {
"line": 29,
"column": 1,
"offset": 580
},
"end": {
"line": 29,
"column": 11,
"offset": 590
},
"indent": []
}
},
{
"type": "heading",
"depth": 2,
"children": [
{
"type": "text",
"value": "Tests",
"position": {
"start": {
"line": 31,
"column": 4,
"offset": 595
},
"end": {
"line": 31,
"column": 9,
"offset": 600
},
"indent": []
}
}
],
"position": {
"start": {
"line": 31,
"column": 1,
"offset": 592
},
"end": {
"line": 31,
"column": 9,
"offset": 600
},
"indent": []
}
},
{
"type": "html",
"value": "<section id='tests'>",
"position": {
"start": {
"line": 32,
"column": 1,
"offset": 601
},
"end": {
"line": 32,
"column": 21,
"offset": 621
},
"indent": []
}
},
{
"type": "code",
"lang": "yml",
"value":
"tests:\n - text: Your <code>h1</code> element should have the text \"Hello World\".\n testString: assert.isTrue((/hello(\\s)+world/gi).test($('h1').text()), 'Your <code>h1</code> element should have the text \"Hello World\".');",
"position": {
"start": {
"line": 34,
"column": 1,
"offset": 623
},
"end": {
"line": 38,
"column": 4,
"offset": 858
},
"indent": [1, 1, 1, 1]
}
},
{
"type": "html",
"value": "</section>",
"position": {
"start": {
"line": 40,
"column": 1,
"offset": 860
},
"end": {
"line": 40,
"column": 11,
"offset": 870
},
"indent": []
}
},
{
"type": "heading",
"depth": 2,
"children": [
{
"type": "text",
"value": "Challenge Seed",
"position": {
"start": {
"line": 42,
"column": 4,
"offset": 875
},
"end": {
"line": 42,
"column": 18,
"offset": 889
},
"indent": []
}
}
],
"position": {
"start": {
"line": 42,
"column": 1,
"offset": 872
},
"end": {
"line": 42,
"column": 18,
"offset": 889
},
"indent": []
}
},
{
"type": "html",
"value": "<section id='challengeSeed'>",
"position": {
"start": {
"line": 43,
"column": 1,
"offset": 890
},
"end": {
"line": 43,
"column": 29,
"offset": 918
},
"indent": []
}
},
{
"type": "html",
"value": "<div id='js-seed'>",
"position": {
"start": {
"line": 45,
"column": 1,
"offset": 920
},
"end": {
"line": 45,
"column": 19,
"offset": 938
},
"indent": []
}
},
{
"type": "code",
"lang": "js",
"value":
"function testFunction(arg) {\n return arg;\n}\n\ntestFunction('hello');",
"position": {
"start": {
"line": 47,
"column": 1,
"offset": 940
},
"end": {
"line": 53,
"column": 4,
"offset": 1018
},
"indent": [1, 1, 1, 1, 1, 1]
}
},
{
"type": "html",
"value": "</div>",
"position": {
"start": {
"line": 55,
"column": 1,
"offset": 1020
},
"end": {
"line": 55,
"column": 7,
"offset": 1026
},
"indent": []
}
},
{
"type": "heading",
"depth": 3,
"children": [
{
"type": "text",
"value": "Before Test",
"position": {
"start": {
"line": 57,
"column": 5,
"offset": 1032
},
"end": {
"line": 57,
"column": 16,
"offset": 1043
},
"indent": []
}
}
],
"position": {
"start": {
"line": 57,
"column": 1,
"offset": 1028
},
"end": {
"line": 57,
"column": 16,
"offset": 1043
},
"indent": []
}
},
{
"type": "html",
"value": "<div id='js-setup'>",
"position": {
"start": {
"line": 58,
"column": 1,
"offset": 1044
},
"end": {
"line": 58,
"column": 20,
"offset": 1063
},
"indent": []
}
},
{
"type": "code",
"lang": "js",
"value": "console.log('before the test');",
"position": {
"start": {
"line": 60,
"column": 1,
"offset": 1065
},
"end": {
"line": 62,
"column": 4,
"offset": 1106
},
"indent": [1, 1]
}
},
{
"type": "html",
"value": "</div>",
"position": {
"start": {
"line": 64,
"column": 1,
"offset": 1108
},
"end": {
"line": 64,
"column": 7,
"offset": 1114
},
"indent": []
}
},
{
"type": "heading",
"depth": 3,
"children": [
{
"type": "text",
"value": "After Test",
"position": {
"start": {
"line": 66,
"column": 5,
"offset": 1120
},
"end": {
"line": 66,
"column": 15,
"offset": 1130
},
"indent": []
}
}
],
"position": {
"start": {
"line": 66,
"column": 1,
"offset": 1116
},
"end": {
"line": 66,
"column": 15,
"offset": 1130
},
"indent": []
}
},
{
"type": "html",
"value": "<div id='js-teardown'>",
"position": {
"start": {
"line": 67,
"column": 1,
"offset": 1131
},
"end": {
"line": 67,
"column": 23,
"offset": 1153
},
"indent": []
}
},
{
"type": "code",
"lang": "js",
"value": "console.info('after the test');",
"position": {
"start": {
"line": 69,
"column": 1,
"offset": 1155
},
"end": {
"line": 71,
"column": 4,
"offset": 1196
},
"indent": [1, 1]
}
},
{
"type": "html",
"value": "</div>\n</section>",
"position": {
"start": {
"line": 73,
"column": 1,
"offset": 1198
},
"end": {
"line": 74,
"column": 11,
"offset": 1215
},
"indent": [1]
}
},
{
"type": "heading",
"depth": 2,
"children": [
{
"type": "text",
"value": "Solution",
"position": {
"start": {
"line": 76,
"column": 4,
"offset": 1220
},
"end": {
"line": 76,
"column": 12,
"offset": 1228
},
"indent": []
}
}
],
"position": {
"start": {
"line": 76,
"column": 1,
"offset": 1217
},
"end": {
"line": 76,
"column": 12,
"offset": 1228
},
"indent": []
}
},
{
"type": "html",
"value": "<section id='solution'>",
"position": {
"start": {
"line": 77,
"column": 1,
"offset": 1229
},
"end": {
"line": 77,
"column": 24,
"offset": 1252
},
"indent": []
}
},
{
"type": "code",
"lang": "js",
"value":
"function testFunction(arg) {\n return arg;\n}\n\ntestFunction('hello');",
"position": {
"start": {
"line": 79,
"column": 1,
"offset": 1254
},
"end": {
"line": 85,
"column": 4,
"offset": 1332
},
"indent": [1, 1, 1, 1, 1, 1]
}
},
{
"type": "html",
"value": "</section>",
"position": {
"start": {
"line": 86,
"column": 1,
"offset": 1333
},
"end": {
"line": 86,
"column": 11,
"offset": 1343
},
"indent": []
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 86,
"column": 11,
"offset": 1343
}
}
}

View File

@ -0,0 +1,18 @@
const visit = require('unist-util-visit');
const YAML = require('js-yaml');
function plugin() {
return transformer;
function transformer(tree, file) {
visit(tree, 'yaml', visitor);
function visitor(node) {
const frontmatter = YAML.load(node.value);
file.data = { ...file.data, ...frontmatter };
}
}
}
module.exports = plugin;

View File

@ -0,0 +1,62 @@
/* global describe it expect beforeEach */
const { isObject } = require('lodash');
const mockAST = require('./fixtures/challenge-md-ast.json');
const frontmatterToData = require('./frontmatter-to-data');
describe('frontmatter-to-data plugin', () => {
const plugin = frontmatterToData();
let file = { data: {} };
beforeEach(() => {
file = { data: {} };
});
it('should return a plugin which is a function', () => {
expect(typeof plugin).toEqual('function');
});
it('should maintain an object for the `file.data` property', () => {
plugin(mockAST, file);
expect(isObject(file.data)).toBe(true);
});
it('should add all keys from frontmatter to the `file.data` property', () => {
const expectedKeys = ['id', 'title', 'challengeType', 'videoUrl'];
plugin(mockAST, file);
const actualKeys = Object.keys(file.data);
expect(actualKeys).toEqual(expectedKeys);
});
it('should not mutate any type held in the frontmatter', () => {
expect.assertions(4);
plugin(mockAST, file);
const { id, title, challengeType, videoUrl } = file.data;
expect(typeof id).toEqual('string');
expect(typeof title).toEqual('string');
expect(typeof challengeType).toEqual('number');
expect(typeof videoUrl).toEqual('string');
});
it('should trim extra whitespace from keys and values', () => {
expect.assertions(7);
plugin(mockAST, file);
const whitespaceRE = /(^\s\S+|\S\s$)/;
const keys = Object.keys(file.data);
keys.forEach(key => expect(whitespaceRE.test(key)).toBe(false));
const values = keys.map(key => file.data[key]);
values
.filter(value => typeof value === 'string')
.forEach(value => expect(whitespaceRE.test(value)).toBe(false));
});
it('should not mutate url strings', () => {
const expectedUrl = 'https://scrimba.com/p/pVMPUv/cE8Gpt2';
plugin(mockAST, file);
expect(file.data.videoUrl).toEqual(expectedUrl);
});
it('should have an output to match the snapshot', () => {
plugin(mockAST, file);
expect(file.data).toMatchSnapshot();
});
});

View File

@ -0,0 +1,32 @@
const unified = require('unified');
const vfile = require('to-vfile');
const report = require('vfile-reporter');
const markdown = require('remark-parse');
const remark2rehype = require('remark-rehype');
const html = require('rehype-stringify');
const frontmatter = require('remark-frontmatter');
const raw = require('rehype-raw');
const frontmatterToData = require('./frontmatter-to-data');
const textToData = require('./text-to-data');
const testsToData = require('./tests-to-data');
const processor = unified()
.use(markdown)
.use(frontmatter, ['yaml'])
.use(frontmatterToData)
.use(testsToData)
.use(remark2rehype, { allowDangerousHTML: true })
.use(raw)
.use(textToData, ['description', 'instructions'])
// the plugins below are just to stop the processor from throwing
// we need to write a compiler that can create graphql nodes
.use(html);
processor.process(vfile.readSync('maybe.md'), function(err, file) {
if (err) {
throw err;
}
console.error(report(file));
console.log(JSON.stringify(file.data, null, 2));
});

View File

@ -0,0 +1,86 @@
---
id: bd7123c8c441eddfaeb5bdef
title: Say Hello to HTML Elements
challengeType: 0
videoUrl: https://scrimba.com/p/pVMPUv/cE8Gpt2
---
## Description
<section id='description'>
Welcome to freeCodeCamp's HTML coding challenges. These will walk you through web development step-by-step.
Lorem Ipsum with `some code`
> Some text in a blockquote
> Some text in a blockquote, with `code`
```html
<p>We aim to preserve this</p>
```
</section>
## Instructions
<section id='instructions'>
To pass the test on this challenge, change your `h1` element's text to say "Hello World".
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: Your <code>h1</code> element should have the text "Hello World".
testString: assert.isTrue((/hello(\s)+world/gi).test($('h1').text()), 'Your <code>h1</code> element should have the text "Hello World".');
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='js-seed'>
```js
function testFunction(arg) {
return arg;
}
testFunction('hello');
```
</div>
### Before Test
<div id='js-setup'>
```js
console.log('before the test');
```
</div>
### After Test
<div id='js-teardown'>
```js
console.info('after the test');
```
</div>
</section>
## Solution
<section id='solution'>
```js
function testFunction(arg) {
return arg;
}
testFunction('hello');
```
</section>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,29 @@
{
"name": "migration",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest",
"test:watch": "jest --watch"
},
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^23.6.0"
},
"dependencies": {
"hast-util-to-html": "^4.0.1",
"js-yaml": "^3.12.0",
"lodash": "^4.17.11",
"rehype-raw": "^3.0.0",
"rehype-stringify": "^4.0.0",
"remark-frontmatter": "^1.3.0",
"remark-parse": "^5.0.0",
"remark-rehype": "^3.0.1",
"to-vfile": "^5.0.1",
"unified": "^7.0.0",
"unist-util-visit": "^1.4.0",
"vfile-reporter": "^5.0.0"
}
}

View File

@ -0,0 +1,23 @@
const visit = require('unist-util-visit');
const YAML = require('js-yaml');
function plugin() {
return transformer;
function transformer(tree, file) {
visit(tree, 'code', visitor);
function visitor(node) {
const { lang, value } = node;
if (lang === 'yml') {
const tests = YAML.load(value);
file.data = {
...file.data,
...tests
};
}
}
}
}
module.exports = plugin;

View File

@ -0,0 +1,36 @@
/* global describe it expect beforeEach */
const mockAST = require('./fixtures/challenge-md-ast.json');
const testsToData = require('./tests-to-data');
describe('tests-to-data plugin', () => {
const plugin = testsToData();
let file = { data: {} };
beforeEach(() => {
file = { data: {} };
});
it('returns a function', () => {
expect(typeof plugin).toEqual('function');
});
it('adds a `tests` property to `file.data`', () => {
plugin(mockAST, file);
expect('tests' in file.data).toBe(true);
});
it('adds test objects to the tests array following a schema', () => {
expect.assertions(3);
plugin(mockAST, file);
const testObject = file.data.tests[0];
expect(Object.keys(testObject).length).toBe(2);
expect(testObject).toHaveProperty('testString');
expect(testObject).toHaveProperty('text');
});
it('should have an output to match the snapshot', () => {
plugin(mockAST, file);
expect(file.data).toMatchSnapshot();
});
});

View File

@ -0,0 +1,32 @@
const visit = require('unist-util-visit');
const toHTML = require('hast-util-to-html');
const sectionFilter = (
{ type, tagName, properties: { id = '' } },
sectionId
) => {
return type === 'element' && tagName === 'section' && id === sectionId;
};
function textToData(sectionIds) {
if (!sectionIds || !Array.isArray(sectionIds) || sectionIds.length <= 0) {
throw new Error("textToData must have an array of section id's supplied");
}
function transformer(tree, file) {
visit(tree, 'element', visitor);
function visitor(node) {
sectionIds.forEach(sectionId => {
if (sectionFilter(node, sectionId)) {
const textArray = toHTML(node);
file.data = {
...file.data,
[sectionId]: textArray
};
}
});
}
}
return transformer;
}
module.exports = textToData;

View File

@ -0,0 +1,72 @@
/* global describe it expect */
const mockAST = require('./fixtures/challenge-html-ast.json');
const textToData = require('./text-to-data');
describe('text-to-data', () => {
const expectedField = 'description';
const otherExpectedField = 'instructions';
const unexpectedField = 'does-not-exis';
let file = { data: {} };
beforeEach(() => {
file = { data: {} };
});
it('should take return a function', () => {
const plugin = textToData(['a-section-id']);
expect(typeof plugin).toEqual('function');
});
it('throws when no argument or the incorrect argument is supplied', () => {
expect.assertions(5);
const expectedError =
"textToData must have an array of section id's supplied";
expect(() => {
textToData();
}).toThrow(expectedError);
expect(() => {
textToData('');
}).toThrow(expectedError);
expect(() => {
textToData({});
}).toThrow(expectedError);
expect(() => {
textToData(1);
}).toThrow(expectedError);
expect(() => {
textToData([]);
}).toThrow(expectedError);
});
it("should only add a value for 'found' section id's", () => {
const plugin = textToData([expectedField, unexpectedField]);
plugin(mockAST, file);
expect(expectedField in file.data && !(unexpectedField in file.data)).toBe(
true
);
});
it('should add a string relating to the section id to `file.data`', () => {
const plugin = textToData([expectedField]);
plugin(mockAST, file);
const expectedText = 'Welcome to freeCodeCamp';
expect(file.data[expectedField].includes(expectedText)).toBe(true);
});
it('should preserve nested html', () => {
const plugin = textToData([expectedField]);
plugin(mockAST, file);
const expectedText = `<blockquote>
<p>Some text in a blockquote</p>
<p>Some text in a blockquote, with <code>code</code></p>
</blockquote>`;
expect(file.data[expectedField].includes(expectedText)).toBe(true);
});
it('should have an output to match the snapshot', () => {
const plugin = textToData([expectedField, otherExpectedField]);
plugin(mockAST, file);
expect(file.data).toMatchSnapshot();
});
});