chore: remove old parser
This commit is contained in:
committed by
Mrugesh Mohapatra
parent
e3511f2930
commit
a3a678b7af
@ -0,0 +1,11 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`process-frontmatter plugin should have an output to match the snapshot 1`] = `
|
||||
Object {
|
||||
"challengeType": 0,
|
||||
"forumTopicId": 18276,
|
||||
"id": "bd7123c8c441eddfaeb5bdef",
|
||||
"title": "Say Hello to HTML Elements",
|
||||
"videoUrl": "https://scrimba.com/p/pVMPUv/cE8Gpt2",
|
||||
}
|
||||
`;
|
@ -0,0 +1,43 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`add-seed plugin should have an output to match the snapshot 1`] = `
|
||||
Object {
|
||||
"files": Object {
|
||||
"indexcss": Object {
|
||||
"contents": "body {
|
||||
background: green;
|
||||
}",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "css",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexcss",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexhtml": Object {
|
||||
"contents": "<html>
|
||||
<body>
|
||||
</body>
|
||||
</html>",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "html",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexhtml",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexjs": Object {
|
||||
"contents": "var x = 'y';",
|
||||
"editableRegionBoundaries": Array [],
|
||||
"ext": "js",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexjs",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
@ -0,0 +1,43 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`add solution plugin should have an output to match the snapshot 1`] = `
|
||||
Object {
|
||||
"solutions": Array [
|
||||
Object {
|
||||
"indexcss": Object {
|
||||
"contents": "body {
|
||||
background: white;
|
||||
}",
|
||||
"ext": "css",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexcss",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexhtml": Object {
|
||||
"contents": "<html>
|
||||
<body>
|
||||
</body>
|
||||
</html>",
|
||||
"ext": "html",
|
||||
"head": "",
|
||||
"id": "html-key",
|
||||
"key": "indexhtml",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
"indexjs": Object {
|
||||
"contents": "var x = 'y';
|
||||
\`\`",
|
||||
"ext": "js",
|
||||
"head": "",
|
||||
"id": "",
|
||||
"key": "indexjs",
|
||||
"name": "index",
|
||||
"tail": "",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
@ -0,0 +1,23 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`add-tests plugin should have an output to match the snapshot 1`] = `
|
||||
Object {
|
||||
"tests": Array [
|
||||
Object {
|
||||
"testString": "// test code",
|
||||
"text": "<p>First hint</p>",
|
||||
},
|
||||
Object {
|
||||
"testString": "// more test code",
|
||||
"text": "<p>Second hint with <code>code</code></p>",
|
||||
},
|
||||
Object {
|
||||
"testString": "// more test code
|
||||
if(let x of xs) {
|
||||
console.log(x);
|
||||
}",
|
||||
"text": "<p>Third <em>hint</em> with <code>code</code> and <code>inline code</code></p>",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
@ -0,0 +1,16 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`add-text should have an output to match the snapshot 1`] = `
|
||||
Object {
|
||||
"description": "<section id=\\"description\\">
|
||||
<p>Paragraph 1</p>
|
||||
<pre><code class=\\"language-html\\">code example
|
||||
</code></pre>
|
||||
</section>",
|
||||
"instructions": "<section id=\\"instructions\\">
|
||||
<p>Paragraph 0</p>
|
||||
<pre><code class=\\"language-html\\">code example 0
|
||||
</code></pre>
|
||||
</section>",
|
||||
}
|
||||
`;
|
@ -0,0 +1,18 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`add-video-question plugin should match the video snapshot 1`] = `
|
||||
Object {
|
||||
"question": Object {
|
||||
"answers": Array [
|
||||
"<p>Some inline <code>code</code></p>",
|
||||
"<p>Some <em>italics</em></p>
|
||||
<p>A second answer paragraph.</p>",
|
||||
"<p><code> code in </code> code tags</p>",
|
||||
],
|
||||
"solution": 3,
|
||||
"text": "<p>Question line 1</p>
|
||||
<pre><code class=\\"language-js\\"> var x = 'y';
|
||||
</code></pre>",
|
||||
},
|
||||
}
|
||||
`;
|
@ -0,0 +1,558 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`replace-imports should have an output to match the snapshot 1`] = `
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 18,
|
||||
"line": 3,
|
||||
"offset": 68,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 3,
|
||||
"line": 3,
|
||||
"offset": 53,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "--description--",
|
||||
},
|
||||
],
|
||||
"depth": 1,
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 18,
|
||||
"line": 3,
|
||||
"offset": 68,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 3,
|
||||
"offset": 51,
|
||||
},
|
||||
},
|
||||
"type": "heading",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 12,
|
||||
"line": 5,
|
||||
"offset": 81,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 5,
|
||||
"offset": 70,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "Paragraph 1",
|
||||
},
|
||||
],
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 12,
|
||||
"line": 5,
|
||||
"offset": 81,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 5,
|
||||
"offset": 70,
|
||||
},
|
||||
},
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"lang": "html",
|
||||
"meta": null,
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 4,
|
||||
"line": 9,
|
||||
"offset": 107,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 7,
|
||||
"offset": 83,
|
||||
},
|
||||
},
|
||||
"type": "code",
|
||||
"value": "code example",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 19,
|
||||
"line": 11,
|
||||
"offset": 127,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 3,
|
||||
"line": 11,
|
||||
"offset": 111,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "--instructions--",
|
||||
},
|
||||
],
|
||||
"depth": 1,
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 19,
|
||||
"line": 11,
|
||||
"offset": 127,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 11,
|
||||
"offset": 109,
|
||||
},
|
||||
},
|
||||
"type": "heading",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 12,
|
||||
"line": 13,
|
||||
"offset": 140,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 13,
|
||||
"offset": 129,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "Paragraph 0",
|
||||
},
|
||||
],
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 12,
|
||||
"line": 13,
|
||||
"offset": 140,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 13,
|
||||
"offset": 129,
|
||||
},
|
||||
},
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"lang": "html",
|
||||
"meta": null,
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 4,
|
||||
"line": 17,
|
||||
"offset": 168,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 15,
|
||||
"offset": 142,
|
||||
},
|
||||
},
|
||||
"type": "code",
|
||||
"value": "code example 0",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 12,
|
||||
"line": 19,
|
||||
"offset": 181,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 3,
|
||||
"line": 19,
|
||||
"offset": 172,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "--hints--",
|
||||
},
|
||||
],
|
||||
"depth": 1,
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 12,
|
||||
"line": 19,
|
||||
"offset": 181,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 19,
|
||||
"offset": 170,
|
||||
},
|
||||
},
|
||||
"type": "heading",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 11,
|
||||
"line": 21,
|
||||
"offset": 193,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 21,
|
||||
"offset": 183,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "First hint",
|
||||
},
|
||||
],
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 11,
|
||||
"line": 21,
|
||||
"offset": 193,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 21,
|
||||
"offset": 183,
|
||||
},
|
||||
},
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"lang": "js",
|
||||
"meta": null,
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 4,
|
||||
"line": 25,
|
||||
"offset": 217,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 23,
|
||||
"offset": 195,
|
||||
},
|
||||
},
|
||||
"type": "code",
|
||||
"value": "// test code",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 18,
|
||||
"line": 27,
|
||||
"offset": 236,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 27,
|
||||
"offset": 219,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "Second hint with ",
|
||||
},
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 24,
|
||||
"line": 27,
|
||||
"offset": 242,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 18,
|
||||
"line": 27,
|
||||
"offset": 236,
|
||||
},
|
||||
},
|
||||
"type": "html",
|
||||
"value": "<code>",
|
||||
},
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 28,
|
||||
"line": 27,
|
||||
"offset": 246,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 24,
|
||||
"line": 27,
|
||||
"offset": 242,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "code",
|
||||
},
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 35,
|
||||
"line": 27,
|
||||
"offset": 253,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 28,
|
||||
"line": 27,
|
||||
"offset": 246,
|
||||
},
|
||||
},
|
||||
"type": "html",
|
||||
"value": "</code>",
|
||||
},
|
||||
],
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 35,
|
||||
"line": 27,
|
||||
"offset": 253,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 27,
|
||||
"offset": 219,
|
||||
},
|
||||
},
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"lang": "js",
|
||||
"meta": null,
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 4,
|
||||
"line": 31,
|
||||
"offset": 282,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 29,
|
||||
"offset": 255,
|
||||
},
|
||||
},
|
||||
"type": "code",
|
||||
"value": "// more test code",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 11,
|
||||
"line": 34,
|
||||
"offset": 295,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 3,
|
||||
"line": 34,
|
||||
"offset": 287,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "--seed--",
|
||||
},
|
||||
],
|
||||
"depth": 1,
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 11,
|
||||
"line": 34,
|
||||
"offset": 295,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 34,
|
||||
"offset": 285,
|
||||
},
|
||||
},
|
||||
"type": "heading",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 21,
|
||||
"line": 36,
|
||||
"offset": 317,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 4,
|
||||
"line": 36,
|
||||
"offset": 300,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "--seed-contents--",
|
||||
},
|
||||
],
|
||||
"depth": 2,
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 21,
|
||||
"line": 36,
|
||||
"offset": 317,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 36,
|
||||
"offset": 297,
|
||||
},
|
||||
},
|
||||
"type": "heading",
|
||||
},
|
||||
Object {
|
||||
"lang": "html",
|
||||
"meta": null,
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 4,
|
||||
"line": 43,
|
||||
"offset": 364,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 38,
|
||||
"offset": 319,
|
||||
},
|
||||
},
|
||||
"type": "code",
|
||||
"value": "<html>
|
||||
<body>
|
||||
</body>
|
||||
</html>",
|
||||
},
|
||||
Object {
|
||||
"lang": "css",
|
||||
"meta": null,
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 4,
|
||||
"line": 49,
|
||||
"offset": 406,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 45,
|
||||
"offset": 366,
|
||||
},
|
||||
},
|
||||
"type": "code",
|
||||
"value": "body {
|
||||
background: green;
|
||||
}",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"alt": "custom-name",
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 17,
|
||||
"line": 51,
|
||||
"offset": 424,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 51,
|
||||
"offset": 408,
|
||||
},
|
||||
},
|
||||
"title": null,
|
||||
"type": "image",
|
||||
"url": "",
|
||||
},
|
||||
],
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 17,
|
||||
"line": 51,
|
||||
"offset": 424,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 51,
|
||||
"offset": 408,
|
||||
},
|
||||
},
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"lang": "js",
|
||||
"meta": null,
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 4,
|
||||
"line": 55,
|
||||
"offset": 448,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 53,
|
||||
"offset": 426,
|
||||
},
|
||||
},
|
||||
"type": "code",
|
||||
"value": "var x = 'y';",
|
||||
},
|
||||
Object {
|
||||
"lang": "js",
|
||||
"meta": null,
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 4,
|
||||
"line": 6,
|
||||
"offset": 125,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 1,
|
||||
"offset": 0,
|
||||
},
|
||||
},
|
||||
"type": "code",
|
||||
"value": "for (let index = 0; index < array.length; index++) {
|
||||
const element = array[index];
|
||||
// imported from script.md
|
||||
}",
|
||||
},
|
||||
],
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 1,
|
||||
"line": 58,
|
||||
"offset": 476,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 1,
|
||||
"offset": 0,
|
||||
},
|
||||
},
|
||||
"type": "root",
|
||||
}
|
||||
`;
|
18
tools/challenge-parser/parser/plugins/add-frontmatter.js
Normal file
18
tools/challenge-parser/parser/plugins/add-frontmatter.js
Normal 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;
|
@ -0,0 +1,72 @@
|
||||
/* global describe it expect beforeEach */
|
||||
|
||||
const { isObject } = require('lodash');
|
||||
|
||||
const mockAST = require('../__fixtures__/ast-yaml-challenge.json');
|
||||
const processFrontmatter = require('./add-frontmatter');
|
||||
|
||||
describe('process-frontmatter plugin', () => {
|
||||
const plugin = processFrontmatter();
|
||||
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);
|
||||
});
|
||||
|
||||
// And no others. The AST includes some yaml code, and this also
|
||||
// checks that none of those keys get parsed
|
||||
it('should add all keys from frontmatter to the `file.data` property', () => {
|
||||
const expectedKeys = [
|
||||
'id',
|
||||
'title',
|
||||
'challengeType',
|
||||
'videoUrl',
|
||||
'forumTopicId'
|
||||
];
|
||||
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(5);
|
||||
plugin(mockAST, file);
|
||||
const { id, title, challengeType, videoUrl, forumTopicId } = file.data;
|
||||
expect(typeof id).toEqual('string');
|
||||
expect(typeof title).toEqual('string');
|
||||
expect(typeof challengeType).toEqual('number');
|
||||
expect(typeof videoUrl).toEqual('string');
|
||||
expect(typeof forumTopicId).toEqual('number');
|
||||
});
|
||||
|
||||
it('should trim extra whitespace from keys and values', () => {
|
||||
expect.assertions(8);
|
||||
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();
|
||||
});
|
||||
});
|
35
tools/challenge-parser/parser/plugins/add-ids.js
Normal file
35
tools/challenge-parser/parser/plugins/add-ids.js
Normal file
@ -0,0 +1,35 @@
|
||||
const visitChildren = require('unist-util-visit-children');
|
||||
|
||||
function hasId(node, index, parent) {
|
||||
// image references should always be inside paragraphs.
|
||||
if (node.type !== 'paragraph') return;
|
||||
const idHolder = node.children[0];
|
||||
if (idHolder.type === 'imageReference') {
|
||||
if (node.children.length > 1) {
|
||||
console.log('oooops, too many links together!');
|
||||
// TODO: optional chaining
|
||||
} else if (
|
||||
parent.children[index + 1] &&
|
||||
parent.children[index + 1].type === 'code'
|
||||
) {
|
||||
console.log('found adjacent code block for id ' + idHolder.identifier);
|
||||
} else {
|
||||
console.log(
|
||||
'ooops! the id ' + idHolder.identifier + ' is not next to a code block'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function plugin() {
|
||||
// we don't want to recurse into the tree, hence visitChildren
|
||||
|
||||
const visit = visitChildren(hasId);
|
||||
return transformer;
|
||||
|
||||
function transformer(tree) {
|
||||
visit(tree);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = plugin;
|
107
tools/challenge-parser/parser/plugins/add-seed.js
Normal file
107
tools/challenge-parser/parser/plugins/add-seed.js
Normal file
@ -0,0 +1,107 @@
|
||||
const getAllBetween = require('./utils/between-headings');
|
||||
// const visit = require('unist-util-visit');
|
||||
const visitChildren = require('unist-util-visit-children');
|
||||
const { root } = require('mdast-builder');
|
||||
const { getFileVisitor } = require('./utils/get-file-visitor');
|
||||
const { isEmpty } = require('lodash');
|
||||
|
||||
const editableRegionMarker = '--fcc-editable-region--';
|
||||
|
||||
function findRegionMarkers(file) {
|
||||
const lines = file.contents.split('\n');
|
||||
const editableLines = lines
|
||||
.map((line, id) => (line.trim() === editableRegionMarker ? id : -1))
|
||||
.filter(id => id >= 0);
|
||||
|
||||
if (editableLines.length > 2) {
|
||||
throw Error('Editable region has too many markers: ' + editableLines);
|
||||
}
|
||||
|
||||
if (editableLines.length === 0) {
|
||||
return null;
|
||||
} else if (editableLines.length === 1) {
|
||||
throw Error(`Editable region not closed`);
|
||||
} else {
|
||||
return editableLines;
|
||||
}
|
||||
}
|
||||
|
||||
function removeLines(contents, toRemove) {
|
||||
const lines = contents.split('\n');
|
||||
return lines.filter((_, id) => !toRemove.includes(id)).join('\n');
|
||||
}
|
||||
|
||||
// TODO: DRY this. Start with an array of markers and go from there.
|
||||
function addSeeds() {
|
||||
function transformer(tree, file) {
|
||||
const seedTree = root(getAllBetween(tree, `--seed--`));
|
||||
// Not all challenges have seeds (video challenges, for example), so we stop
|
||||
// processing in these cases.
|
||||
if (isEmpty(seedTree.children)) return;
|
||||
const contentsTree = root(getAllBetween(seedTree, `--seed-contents--`));
|
||||
const headTree = root(getAllBetween(seedTree, `--before-user-code--`));
|
||||
const tailTree = root(getAllBetween(seedTree, `--after-user-code--`));
|
||||
const seeds = {};
|
||||
|
||||
// While before and after code are optional, the contents are not
|
||||
if (isEmpty(contentsTree.children))
|
||||
throw Error('## --seed-contents-- must appear in # --seed-- sections');
|
||||
|
||||
const visitForContents = visitChildren(
|
||||
getFileVisitor(seeds, 'contents', validateEditableMarkers)
|
||||
);
|
||||
const visitForHead = visitChildren(getFileVisitor(seeds, 'head'));
|
||||
const visitForTail = visitChildren(getFileVisitor(seeds, 'tail'));
|
||||
visitForContents(contentsTree);
|
||||
visitForHead(headTree);
|
||||
visitForTail(tailTree);
|
||||
file.data = {
|
||||
...file.data,
|
||||
files: seeds
|
||||
};
|
||||
|
||||
// process region markers - remove them from content and add them to data
|
||||
Object.keys(seeds).forEach(key => {
|
||||
const fileData = seeds[key];
|
||||
const editRegionMarkers = findRegionMarkers(fileData);
|
||||
if (editRegionMarkers) {
|
||||
fileData.contents = removeLines(fileData.contents, editRegionMarkers);
|
||||
|
||||
if (editRegionMarkers[1] <= editRegionMarkers[0]) {
|
||||
throw Error('Editable region must be non zero');
|
||||
}
|
||||
fileData.editableRegionBoundaries = editRegionMarkers;
|
||||
} else {
|
||||
fileData.editableRegionBoundaries = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return transformer;
|
||||
}
|
||||
|
||||
function validateEditableMarkers({ value, position }) {
|
||||
const twoMarkersRE = RegExp(
|
||||
editableRegionMarker + '.*' + editableRegionMarker
|
||||
);
|
||||
const formattedMarkerRE = /--fcc - editable - region--/;
|
||||
const lines = value.split('\n');
|
||||
const baseLineNumber = position.start.line + 1;
|
||||
lines.forEach((line, index) => {
|
||||
if (line.match(twoMarkersRE)) {
|
||||
throw Error(
|
||||
`Line ${baseLineNumber +
|
||||
index} has two markers. Each line should only have one.`
|
||||
);
|
||||
}
|
||||
if (line.match(formattedMarkerRE)) {
|
||||
throw Error(
|
||||
`Line ${baseLineNumber +
|
||||
index} has a malformed marker. It should be --fcc-editable-region--`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = addSeeds;
|
||||
module.exports.editableRegionMarker = editableRegionMarker;
|
287
tools/challenge-parser/parser/plugins/add-seed.test.js
Normal file
287
tools/challenge-parser/parser/plugins/add-seed.test.js
Normal file
@ -0,0 +1,287 @@
|
||||
/* global describe it expect beforeEach */
|
||||
const isArray = require('lodash/isArray');
|
||||
|
||||
const simpleAST = require('../__fixtures__/ast-simple.json');
|
||||
const withEditableAST = require('../__fixtures__/ast-with-markers.json');
|
||||
const withSeedKeysAST = require('../__fixtures__/ast-seed-keys.json');
|
||||
const withExtraLinesAST = require('../__fixtures__/ast-with-extra-lines.json');
|
||||
const orphanKeyAST = require('../__fixtures__/ast-orphan-key.json');
|
||||
const adjacentKeysAST = require('../__fixtures__/ast-adjacent-keys.json');
|
||||
const withBeforeAfterAST = require('../__fixtures__/ast-before-after.json');
|
||||
const emptyBeforeAST = require('../__fixtures__/ast-empty-before.json');
|
||||
const emptyAfterAST = require('../__fixtures__/ast-empty-after.json');
|
||||
const emptyCSSAST = require('../__fixtures__/ast-empty-css.json');
|
||||
const emptyHTMLAST = require('../__fixtures__/ast-empty-html.json');
|
||||
const doubleMarkerAST = require('../__fixtures__/ast-double-marker.json');
|
||||
const jsxSeedAST = require('../__fixtures__/ast-jsx-seed.json');
|
||||
const cCodeAST = require('../__fixtures__/ast-c-code.json');
|
||||
const explodedMarkerAST = require('../__fixtures__/ast-exploded-marker.json');
|
||||
const emptyContentAST = require('../__fixtures__/ast-empty-contents.json');
|
||||
|
||||
const addSeed = require('./add-seed');
|
||||
const { isObject } = require('lodash');
|
||||
|
||||
describe('add-seed plugin', () => {
|
||||
const plugin = addSeed();
|
||||
let file = { data: {} };
|
||||
|
||||
beforeEach(() => {
|
||||
file = { data: {} };
|
||||
});
|
||||
|
||||
it('returns a function', () => {
|
||||
expect(typeof plugin).toEqual('function');
|
||||
});
|
||||
|
||||
it('adds a `files` property to `file.data`', () => {
|
||||
plugin(simpleAST, file);
|
||||
expect('files' in file.data).toBe(true);
|
||||
});
|
||||
|
||||
it('ensures that the `files` property is an object', () => {
|
||||
plugin(simpleAST, file);
|
||||
expect(isObject(file.data.files)).toBe(true);
|
||||
});
|
||||
|
||||
it('adds test objects to the files array following a schema', () => {
|
||||
expect.assertions(17);
|
||||
plugin(simpleAST, file);
|
||||
const {
|
||||
data: { files }
|
||||
} = file;
|
||||
const testObject = files.indexjs;
|
||||
expect(Object.keys(testObject).length).toEqual(8);
|
||||
expect(testObject).toHaveProperty('key');
|
||||
expect(typeof testObject['key']).toBe('string');
|
||||
expect(testObject).toHaveProperty('ext');
|
||||
expect(typeof testObject['ext']).toBe('string');
|
||||
expect(testObject).toHaveProperty('name');
|
||||
expect(typeof testObject['name']).toBe('string');
|
||||
expect(testObject).toHaveProperty('contents');
|
||||
expect(typeof testObject['contents']).toBe('string');
|
||||
expect(testObject).toHaveProperty('head');
|
||||
expect(typeof testObject['head']).toBe('string');
|
||||
expect(testObject).toHaveProperty('tail');
|
||||
expect(typeof testObject['tail']).toBe('string');
|
||||
expect(testObject).toHaveProperty('id');
|
||||
expect(typeof testObject['id']).toBe('string');
|
||||
expect(testObject).toHaveProperty('editableRegionBoundaries');
|
||||
expect(isArray(testObject['editableRegionBoundaries'])).toBe(true);
|
||||
});
|
||||
|
||||
it('parses seeds without ids', () => {
|
||||
expect.assertions(6);
|
||||
plugin(simpleAST, file);
|
||||
const {
|
||||
data: { files }
|
||||
} = file;
|
||||
const { indexjs, indexhtml, indexcss } = files;
|
||||
|
||||
expect(indexjs.contents).toBe(`var x = 'y';`);
|
||||
expect(indexjs.key).toBe(`indexjs`);
|
||||
expect(indexhtml.contents).toBe(`<html>
|
||||
<body>
|
||||
</body>
|
||||
</html>`);
|
||||
expect(indexhtml.key).toBe(`indexhtml`);
|
||||
expect(indexcss.contents).toBe(`body {
|
||||
background: green;
|
||||
}`);
|
||||
expect(indexcss.key).toBe(`indexcss`);
|
||||
});
|
||||
|
||||
it('removes region markers from contents', () => {
|
||||
expect.assertions(2);
|
||||
plugin(withEditableAST, file);
|
||||
const {
|
||||
data: { files }
|
||||
} = file;
|
||||
const { indexcss } = files;
|
||||
|
||||
expect(indexcss.contents).not.toMatch('--fcc-editable-region--');
|
||||
expect(indexcss.editableRegionBoundaries).toEqual([1, 4]);
|
||||
});
|
||||
|
||||
// TODO: can we reuse 'name'? It's always 'index', I think, which suggests
|
||||
// it could be reused as an id. Revisit this once the parser is live.
|
||||
it('parses seeds with adjacent ids, adding the id to data', () => {
|
||||
expect.assertions(3);
|
||||
plugin(withSeedKeysAST, file);
|
||||
const {
|
||||
data: { files }
|
||||
} = file;
|
||||
const { indexhtml, indexcss, indexjs } = files;
|
||||
|
||||
expect(indexhtml.id).toBe('');
|
||||
expect(indexcss.id).toBe('key-for-css');
|
||||
expect(indexjs.id).toBe('key-for-js');
|
||||
});
|
||||
|
||||
it('throws if an id is anywhere except directly before a code node', () => {
|
||||
expect.assertions(2);
|
||||
expect(() => plugin(adjacentKeysAST, file)).toThrow(
|
||||
'::id{#id}s must come before code blocks'
|
||||
);
|
||||
expect(() => plugin(orphanKeyAST, file)).toThrow(
|
||||
'::id{#id}s must come before code blocks'
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores empty lines between ::id{#id}s and code blocks', () => {
|
||||
expect.assertions(1);
|
||||
plugin(withSeedKeysAST, file);
|
||||
const fileTwo = { data: {} };
|
||||
plugin(withExtraLinesAST, fileTwo);
|
||||
expect(file).toEqual(fileTwo);
|
||||
});
|
||||
|
||||
it('gets the before-user-code for each language', () => {
|
||||
expect.assertions(3);
|
||||
plugin(withBeforeAfterAST, file);
|
||||
const {
|
||||
data: { files }
|
||||
} = file;
|
||||
const { indexjs, indexhtml, indexcss } = files;
|
||||
|
||||
expect(indexjs.head).toBe('');
|
||||
expect(indexhtml.head).toBe(`<!-- comment -->`);
|
||||
expect(indexcss.head).toBe(`body {
|
||||
etc: ''
|
||||
}`);
|
||||
});
|
||||
|
||||
it('gets the after-user-code for each language', () => {
|
||||
expect.assertions(3);
|
||||
plugin(withBeforeAfterAST, file);
|
||||
const {
|
||||
data: { files }
|
||||
} = file;
|
||||
const { indexjs, indexhtml, indexcss } = files;
|
||||
|
||||
expect(indexjs.tail).toBe(`function teardown(params) {
|
||||
// after
|
||||
}`);
|
||||
expect(indexhtml.tail).toBe('');
|
||||
expect(indexcss.tail).toBe(`body {
|
||||
background: blue;
|
||||
}`);
|
||||
});
|
||||
|
||||
it('throws an error if there is any code of an unsupported language', () => {
|
||||
expect.assertions(1);
|
||||
expect(() => plugin(cCodeAST, file)).toThrow(
|
||||
"On line 30 'c' is not a supported language.\n" +
|
||||
' Please use one of js, css, html, jsx or py'
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if there is before/after code with empty blocks', () => {
|
||||
expect.assertions(2);
|
||||
expect(() => plugin(emptyHTMLAST, file)).toThrow(
|
||||
'Empty code block in --before-user-code-- section'
|
||||
);
|
||||
expect(() => plugin(emptyCSSAST, file)).toThrow(
|
||||
'Empty code block in --after-user-code-- section'
|
||||
);
|
||||
});
|
||||
|
||||
it('quietly ignores empty before sections', () => {
|
||||
expect.assertions(6);
|
||||
plugin(emptyBeforeAST, file);
|
||||
const {
|
||||
data: { files }
|
||||
} = file;
|
||||
const { indexjs, indexhtml, indexcss } = files;
|
||||
|
||||
expect(indexjs.head).toBe('');
|
||||
expect(indexjs.tail).toBe('function teardown(params) {\n // after\n}');
|
||||
expect(indexhtml.head).toBe('');
|
||||
expect(indexhtml.tail).toBe('');
|
||||
expect(indexcss.head).toBe('');
|
||||
expect(indexcss.tail).toBe('body {\n background: blue;\n}');
|
||||
});
|
||||
|
||||
it('quietly ignores empty after sections', () => {
|
||||
expect.assertions(6);
|
||||
plugin(emptyAfterAST, file);
|
||||
const {
|
||||
data: { files }
|
||||
} = file;
|
||||
const { indexjs, indexhtml, indexcss } = files;
|
||||
|
||||
expect(indexjs.head).toBe('');
|
||||
expect(indexjs.tail).toBe('');
|
||||
expect(indexhtml.head).toBe('<!-- comment -->');
|
||||
expect(indexhtml.tail).toBe('');
|
||||
expect(indexcss.head).toBe("body {\n etc: ''\n}");
|
||||
expect(indexcss.tail).toBe('');
|
||||
});
|
||||
|
||||
it('throws an error (with line number) if 2 markers appear on 1 line', () => {
|
||||
expect.assertions(1);
|
||||
expect(() => plugin(doubleMarkerAST, file)).toThrow(
|
||||
`Line 8 has two markers. Each line should only have one.`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if a javascript file has formatted a marker', () => {
|
||||
expect.assertions(1);
|
||||
expect(() => plugin(explodedMarkerAST, file)).toThrow(
|
||||
`Line 66 has a malformed marker. It should be --fcc-editable-region--`
|
||||
);
|
||||
});
|
||||
|
||||
it('handles jsx', () => {
|
||||
expect.assertions(4);
|
||||
plugin(jsxSeedAST, file);
|
||||
const {
|
||||
data: { files }
|
||||
} = file;
|
||||
const { indexjsx } = files;
|
||||
|
||||
expect(indexjsx.head).toBe(`function setup() {}`);
|
||||
expect(indexjsx.tail).toBe(`function teardown(params) {
|
||||
// after
|
||||
}`);
|
||||
expect(indexjsx.contents).toBe(`var x = 'y';
|
||||
|
||||
/* comment */
|
||||
const Button = () => {
|
||||
return <button> {/* another comment! */} text </button>;
|
||||
};`);
|
||||
expect(indexjsx.key).toBe(`indexjsx`);
|
||||
});
|
||||
|
||||
it('combines all the code of a specific language into a single file', () => {
|
||||
/* Revisit this once we've decided what to do about multifile imports. I
|
||||
think the best approach is likely to be use the following format for .files
|
||||
|
||||
{ css: [css files],
|
||||
html: [html files],
|
||||
...
|
||||
}
|
||||
|
||||
or
|
||||
|
||||
{ css: {css files},
|
||||
html: {html files},
|
||||
...
|
||||
}
|
||||
|
||||
depending on what's easier to work with in graphQL
|
||||
|
||||
*/
|
||||
});
|
||||
|
||||
it('should throw an error if a seed has no contents', () => {
|
||||
expect.assertions(1);
|
||||
expect(() => plugin(emptyContentAST, file)).toThrow(
|
||||
`## --seed-contents-- must appear in # --seed-- sections`
|
||||
);
|
||||
});
|
||||
|
||||
it('should have an output to match the snapshot', () => {
|
||||
plugin(simpleAST, file);
|
||||
expect(file.data).toMatchSnapshot();
|
||||
});
|
||||
});
|
43
tools/challenge-parser/parser/plugins/add-solution.js
Normal file
43
tools/challenge-parser/parser/plugins/add-solution.js
Normal file
@ -0,0 +1,43 @@
|
||||
const visitChildren = require('unist-util-visit-children');
|
||||
const { root } = require('mdast-builder');
|
||||
|
||||
const { getFileVisitor } = require('./utils/get-file-visitor');
|
||||
const { splitOnThematicBreak } = require('./utils/split-on-thematic-break');
|
||||
const getAllBetween = require('./utils/between-headings');
|
||||
const { editableRegionMarker } = require('./add-seed');
|
||||
|
||||
function validateMarkers({ value }) {
|
||||
const lines = value.split('\n');
|
||||
if (lines.some(line => line.match(RegExp(editableRegionMarker))))
|
||||
throw Error(
|
||||
'--fcc-editable-region-- should only appear in the --seed-contents--\n' +
|
||||
'section, not in --solutions--'
|
||||
);
|
||||
}
|
||||
|
||||
function createPlugin() {
|
||||
return function transformer(tree, file) {
|
||||
const solutionArrays = splitOnThematicBreak(
|
||||
getAllBetween(tree, `--solutions--`)
|
||||
);
|
||||
const solutions = [];
|
||||
|
||||
solutionArrays.forEach(nodes => {
|
||||
const solution = {};
|
||||
const solutionTree = root(nodes);
|
||||
const visitForContents = visitChildren(
|
||||
getFileVisitor(solution, 'contents', validateMarkers)
|
||||
);
|
||||
|
||||
visitForContents(solutionTree);
|
||||
solutions.push(solution);
|
||||
});
|
||||
|
||||
file.data = {
|
||||
...file.data,
|
||||
solutions: solutions
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = createPlugin;
|
92
tools/challenge-parser/parser/plugins/add-solution.test.js
Normal file
92
tools/challenge-parser/parser/plugins/add-solution.test.js
Normal file
@ -0,0 +1,92 @@
|
||||
/* global describe it expect beforeEach */
|
||||
const mockAST = require('../__fixtures__/ast-simple.json');
|
||||
const editableSolutionAST = require('../__fixtures__/ast-erm-in-solution.json');
|
||||
const multiSolnsAST = require('../__fixtures__/ast-multiple-solutions.json');
|
||||
|
||||
const addSolution = require('./add-solution');
|
||||
const { isObject } = require('lodash');
|
||||
|
||||
describe('add solution plugin', () => {
|
||||
const plugin = addSolution();
|
||||
let file = { data: {} };
|
||||
|
||||
beforeEach(() => {
|
||||
file = { data: {} };
|
||||
});
|
||||
|
||||
it('returns a function', () => {
|
||||
expect(typeof plugin).toEqual('function');
|
||||
});
|
||||
|
||||
it('adds a `solutions` property to `file.data`', () => {
|
||||
plugin(mockAST, file);
|
||||
expect('solutions' in file.data).toBe(true);
|
||||
});
|
||||
|
||||
it('ensures that the `solutions` property is an array', () => {
|
||||
plugin(mockAST, file);
|
||||
expect(Array.isArray(file.data.solutions)).toBe(true);
|
||||
});
|
||||
|
||||
it('each entry in the `solutions` array is an object', () => {
|
||||
plugin(mockAST, file);
|
||||
|
||||
expect(file.data.solutions.every(el => isObject(el))).toBe(true);
|
||||
});
|
||||
|
||||
it('adds solution objects to the files array following a schema', () => {
|
||||
expect.assertions(15);
|
||||
plugin(mockAST, file);
|
||||
const {
|
||||
data: { solutions }
|
||||
} = file;
|
||||
const testObject = solutions[0].indexjs;
|
||||
expect(Object.keys(testObject).length).toEqual(7);
|
||||
expect(testObject).toHaveProperty('key');
|
||||
expect(typeof testObject['key']).toBe('string');
|
||||
expect(testObject).toHaveProperty('ext');
|
||||
expect(typeof testObject['ext']).toBe('string');
|
||||
expect(testObject).toHaveProperty('name');
|
||||
expect(typeof testObject['name']).toBe('string');
|
||||
expect(testObject).toHaveProperty('contents');
|
||||
expect(typeof testObject['contents']).toBe('string');
|
||||
expect(testObject).toHaveProperty('head');
|
||||
expect(typeof testObject['head']).toBe('string');
|
||||
expect(testObject).toHaveProperty('tail');
|
||||
expect(typeof testObject['tail']).toBe('string');
|
||||
expect(testObject).toHaveProperty('id');
|
||||
expect(typeof testObject['id']).toBe('string');
|
||||
});
|
||||
|
||||
it('adds multiple solutions if they exist', () => {
|
||||
expect.assertions(5);
|
||||
plugin(multiSolnsAST, file);
|
||||
const {
|
||||
data: { solutions }
|
||||
} = file;
|
||||
expect(solutions.length).toBe(3);
|
||||
expect(solutions[0].indexjs.contents).toBe("var x = 'y';");
|
||||
expect(solutions[1].indexhtml.contents).toBe(`<html>
|
||||
<body>
|
||||
solution number two
|
||||
</body>
|
||||
</html>`);
|
||||
expect(solutions[1].indexcss.contents).toBe(`body {
|
||||
background: white;
|
||||
}`);
|
||||
expect(solutions[2].indexjs.contents).toBe("var x = 'y3';");
|
||||
});
|
||||
|
||||
it('should reject solutions with editable region markers', () => {
|
||||
expect.assertions(1);
|
||||
expect(() => plugin(editableSolutionAST, file)).toThrow(
|
||||
'--fcc-editable-region-- should only appear in the --seed-contents--\n' +
|
||||
'section, not in --solutions--'
|
||||
);
|
||||
});
|
||||
|
||||
it('should have an output to match the snapshot', () => {
|
||||
plugin(mockAST, file);
|
||||
expect(file.data).toMatchSnapshot();
|
||||
});
|
||||
});
|
32
tools/challenge-parser/parser/plugins/add-tests.js
Normal file
32
tools/challenge-parser/parser/plugins/add-tests.js
Normal file
@ -0,0 +1,32 @@
|
||||
const chunk = require('lodash/chunk');
|
||||
const getAllBetween = require('./utils/between-headings');
|
||||
const mdastToHtml = require('./utils/mdast-to-html');
|
||||
|
||||
function plugin() {
|
||||
return transformer;
|
||||
|
||||
function transformer(tree, file) {
|
||||
const hintNodes = getAllBetween(tree, '--hints--');
|
||||
if (hintNodes.length % 2 !== 0)
|
||||
throw Error('Tests must be in (text, ```testString```) order');
|
||||
|
||||
const tests = chunk(hintNodes, 2).map(getTest);
|
||||
file.data.tests = tests;
|
||||
}
|
||||
}
|
||||
|
||||
function getTest(hintNodes) {
|
||||
const [textNode, testStringNode] = hintNodes;
|
||||
const text = mdastToHtml([textNode]);
|
||||
const testString = testStringNode.value;
|
||||
|
||||
if (!text) throw Error('text is missing from hint');
|
||||
// stub tests (i.e. text, but no testString) are allowed, but the md must
|
||||
// have a code block, even if it is empty.
|
||||
if (!testString && testString !== '')
|
||||
throw Error('testString (code block) is missing from hint');
|
||||
|
||||
return { text, testString };
|
||||
}
|
||||
|
||||
module.exports = plugin;
|
71
tools/challenge-parser/parser/plugins/add-tests.test.js
Normal file
71
tools/challenge-parser/parser/plugins/add-tests.test.js
Normal file
@ -0,0 +1,71 @@
|
||||
/* global describe it expect beforeEach */
|
||||
const simpleAST = require('../__fixtures__/ast-simple.json');
|
||||
const brokenHintsAST = require('../__fixtures__/ast-broken-hints.json');
|
||||
const addTests = require('./add-tests');
|
||||
|
||||
describe('add-tests plugin', () => {
|
||||
const plugin = addTests();
|
||||
let file = { data: {} };
|
||||
|
||||
beforeEach(() => {
|
||||
file = { data: {} };
|
||||
});
|
||||
|
||||
it('returns a function', () => {
|
||||
expect(typeof plugin).toEqual('function');
|
||||
});
|
||||
|
||||
it('adds a `tests` property to `file.data`', () => {
|
||||
plugin(simpleAST, file);
|
||||
|
||||
expect('tests' in file.data).toBe(true);
|
||||
});
|
||||
|
||||
it('adds test objects to the tests array following a schema', () => {
|
||||
expect.assertions(5);
|
||||
plugin(simpleAST, file);
|
||||
const testObject = file.data.tests[0];
|
||||
expect(Object.keys(testObject).length).toBe(2);
|
||||
expect(testObject).toHaveProperty('testString');
|
||||
expect(typeof testObject.testString).toBe('string');
|
||||
expect(testObject).toHaveProperty('text');
|
||||
expect(typeof testObject.text).toBe('string');
|
||||
});
|
||||
|
||||
// TODO: make this a bit more robust and informative
|
||||
it('should throw if a test pair is out of order', () => {
|
||||
expect.assertions(1);
|
||||
expect(() => plugin(brokenHintsAST, file)).toThrow(
|
||||
'testString (code block) is missing from hint'
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves code whitespace in testStrings', () => {
|
||||
plugin(simpleAST, file);
|
||||
const testObject = file.data.tests[2];
|
||||
expect(testObject.testString).toBe(`// more test code
|
||||
if(let x of xs) {
|
||||
console.log(x);
|
||||
}`);
|
||||
});
|
||||
|
||||
it('does not encode html', () => {
|
||||
plugin(simpleAST, file);
|
||||
const testObject = file.data.tests[1];
|
||||
expect(testObject.text).toBe('<p>Second hint with <code>code</code></p>');
|
||||
});
|
||||
|
||||
it('converts test text from md to html', () => {
|
||||
plugin(simpleAST, file);
|
||||
const testObject = file.data.tests[2];
|
||||
expect(testObject.text).toBe(
|
||||
'<p>Third <em>hint</em> with <code>code</code>' +
|
||||
' and <code>inline code</code></p>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should have an output to match the snapshot', () => {
|
||||
plugin(simpleAST, file);
|
||||
expect(file.data).toMatchSnapshot();
|
||||
});
|
||||
});
|
27
tools/challenge-parser/parser/plugins/add-text.js
Normal file
27
tools/challenge-parser/parser/plugins/add-text.js
Normal file
@ -0,0 +1,27 @@
|
||||
const { isEmpty } = require('lodash');
|
||||
const getAllBetween = require('./utils/between-headings');
|
||||
const mdastToHTML = require('./utils/mdast-to-html');
|
||||
|
||||
function addText(sectionIds) {
|
||||
if (!sectionIds || !Array.isArray(sectionIds) || sectionIds.length <= 0) {
|
||||
throw new Error('addText must have an array of section ids supplied');
|
||||
}
|
||||
function transformer(tree, file) {
|
||||
for (const sectionId of sectionIds) {
|
||||
const textNodes = getAllBetween(tree, `--${sectionId}--`);
|
||||
const sectionText = mdastToHTML(textNodes);
|
||||
|
||||
if (!isEmpty(sectionText)) {
|
||||
file.data = {
|
||||
...file.data,
|
||||
[sectionId]: `<section id="${sectionId}">
|
||||
${sectionText}
|
||||
</section>`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return transformer;
|
||||
}
|
||||
|
||||
module.exports = addText;
|
130
tools/challenge-parser/parser/plugins/add-text.test.js
Normal file
130
tools/challenge-parser/parser/plugins/add-text.test.js
Normal file
@ -0,0 +1,130 @@
|
||||
/* global describe it expect */
|
||||
const mockAST = require('../__fixtures__/ast-simple.json');
|
||||
// eslint-disable-next-line max-len
|
||||
const incorrectMarkersAST = require('../__fixtures__/ast-incorrect-markers.json');
|
||||
const realisticAST = require('../__fixtures__/ast-realistic.json');
|
||||
const addText = require('./add-text');
|
||||
|
||||
describe('add-text', () => {
|
||||
const descriptionId = 'description';
|
||||
const instructionsId = 'instructions';
|
||||
// const unexpectedField = 'does-not-exist';
|
||||
let file = { data: {} };
|
||||
|
||||
beforeEach(() => {
|
||||
file = { data: {} };
|
||||
});
|
||||
|
||||
it('should return a function', () => {
|
||||
const plugin = addText(['section']);
|
||||
|
||||
expect(typeof plugin).toEqual('function');
|
||||
});
|
||||
|
||||
it('throws when no argument or the incorrect argument is supplied', () => {
|
||||
expect.assertions(5);
|
||||
const expectedError = 'addText must have an array of section ids supplied';
|
||||
expect(() => {
|
||||
addText();
|
||||
}).toThrow(expectedError);
|
||||
expect(() => {
|
||||
addText('');
|
||||
}).toThrow(expectedError);
|
||||
expect(() => {
|
||||
addText({});
|
||||
}).toThrow(expectedError);
|
||||
expect(() => {
|
||||
addText(1);
|
||||
}).toThrow(expectedError);
|
||||
expect(() => {
|
||||
addText([]);
|
||||
}).toThrow(expectedError);
|
||||
});
|
||||
|
||||
it('should add a string relating to the section id to `file.data`', () => {
|
||||
const plugin = addText([descriptionId]);
|
||||
plugin(mockAST, file);
|
||||
const expectedText = 'Paragraph 1';
|
||||
expect(file.data[descriptionId]).toEqual(
|
||||
expect.stringContaining(expectedText)
|
||||
);
|
||||
});
|
||||
|
||||
it('should not add a string relating a different id to `file.data`', () => {
|
||||
const plugin = addText([descriptionId]);
|
||||
plugin(mockAST, file);
|
||||
// the following text is in the AST, but is associated with a different
|
||||
// marker (instructions)
|
||||
const expectedText = 'Paragraph 0';
|
||||
expect(file.data[descriptionId]).not.toEqual(
|
||||
expect.stringContaining(expectedText)
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: do we need to add the ids to the section tags? Why not just have
|
||||
// <section>?
|
||||
it('should embed the text in sections with appropriate ids', () => {
|
||||
const plugin = addText([descriptionId, instructionsId]);
|
||||
plugin(mockAST, file);
|
||||
// the following text is in the AST, but is associated with a different
|
||||
// marker (instructions)
|
||||
const descriptionSectionText = `<section id="description">
|
||||
<p>Paragraph 1</p>
|
||||
<pre><code class="language-html">code example
|
||||
</code></pre>
|
||||
</section>`;
|
||||
expect(file.data[descriptionId]).toEqual(descriptionSectionText);
|
||||
const instructionsSectionText = `<section id="instructions">
|
||||
<p>Paragraph 0</p>
|
||||
<pre><code class="language-html">code example 0
|
||||
</code></pre>
|
||||
</section>`;
|
||||
expect(file.data[instructionsId]).toBe(instructionsSectionText);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
it('should add nothing if a section id does not appear in the md', () => {
|
||||
const plugin = addText([descriptionId]);
|
||||
plugin(incorrectMarkersAST, file);
|
||||
expect(file.data[descriptionId]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not escape html', () => {
|
||||
const plugin = addText([descriptionId]);
|
||||
plugin(realisticAST, file);
|
||||
const expected = `last <code>h2</code> element`;
|
||||
expect(file.data[descriptionId]).toEqual(expect.stringContaining(expected));
|
||||
});
|
||||
|
||||
it('should preserve nested html', () => {
|
||||
const plugin = addText([descriptionId]);
|
||||
plugin(realisticAST, file);
|
||||
const expected = `<blockquote>
|
||||
<p>Some text in a blockquote</p>
|
||||
<p>
|
||||
Some text in a blockquote, with <code>code</code>
|
||||
</p>
|
||||
</blockquote>`;
|
||||
expect(file.data[descriptionId]).toEqual(expect.stringContaining(expected));
|
||||
});
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
it('should not add paragraphs when html elements are separated by whitespace', () => {
|
||||
const plugin = addText([instructionsId]);
|
||||
plugin(realisticAST, file);
|
||||
const expectedText1 = `<code>code</code> <tag>with more after a space</tag>`;
|
||||
const expectedText2 = `another pair of <strong>elements</strong> <em>with a space</em>`;
|
||||
expect(file.data[instructionsId]).toEqual(
|
||||
expect.stringContaining(expectedText1)
|
||||
);
|
||||
expect(file.data[instructionsId]).toEqual(
|
||||
expect.stringContaining(expectedText2)
|
||||
);
|
||||
});
|
||||
|
||||
it('should have an output to match the snapshot', () => {
|
||||
const plugin = addText([descriptionId, instructionsId]);
|
||||
plugin(mockAST, file);
|
||||
expect(file.data).toMatchSnapshot();
|
||||
});
|
||||
});
|
63
tools/challenge-parser/parser/plugins/add-video-question.js
Normal file
63
tools/challenge-parser/parser/plugins/add-video-question.js
Normal file
@ -0,0 +1,63 @@
|
||||
const { root } = require('mdast-builder');
|
||||
const getAllBetween = require('./utils/between-headings');
|
||||
const mdastToHtml = require('./utils/mdast-to-html');
|
||||
|
||||
const { splitOnThematicBreak } = require('./utils/split-on-thematic-break');
|
||||
|
||||
function plugin() {
|
||||
return transformer;
|
||||
|
||||
function transformer(tree, file) {
|
||||
const questionNodes = getAllBetween(tree, '--question--');
|
||||
if (questionNodes.length > 0) {
|
||||
const questionTree = root(questionNodes);
|
||||
const textNodes = getAllBetween(questionTree, '--text--');
|
||||
const answersNodes = getAllBetween(questionTree, '--answers--');
|
||||
const solutionNodes = getAllBetween(questionTree, '--video-solution--');
|
||||
|
||||
const question = getQuestion(textNodes, answersNodes, solutionNodes);
|
||||
|
||||
file.data.question = question;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getQuestion(textNodes, answersNodes, solutionNodes) {
|
||||
const text = mdastToHtml(textNodes);
|
||||
const answers = getAnswers(answersNodes);
|
||||
const solution = getSolution(solutionNodes);
|
||||
|
||||
if (!text) throw Error('text is missing from question');
|
||||
if (!answers) throw Error('answers are missing from question');
|
||||
if (!solution) throw Error('solution is missing from question');
|
||||
|
||||
// console.log({ text, answers, solution });
|
||||
return { text, answers, solution };
|
||||
}
|
||||
|
||||
function getAnswers(answersNodes) {
|
||||
const answerGroups = splitOnThematicBreak(answersNodes);
|
||||
return answerGroups.map(answer => mdastToHtml(answer));
|
||||
}
|
||||
|
||||
function getSolution(solutionNodes) {
|
||||
let solution;
|
||||
try {
|
||||
if (solutionNodes.length > 1) throw Error('Too many nodes');
|
||||
if (solutionNodes[0].children.length > 1)
|
||||
throw Error('Too many child nodes');
|
||||
const solutionString = solutionNodes[0].children[0].value;
|
||||
if (solutionString === '') throw Error('Non-empty string required');
|
||||
|
||||
solution = Number(solutionString);
|
||||
if (Number.isNaN(solution)) throw Error('Not a number');
|
||||
if (solution < 1) throw Error('Not positive number');
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw Error('A video solution should be a positive integer');
|
||||
}
|
||||
|
||||
return solution;
|
||||
}
|
||||
|
||||
module.exports = plugin;
|
@ -0,0 +1,74 @@
|
||||
/* global describe it expect beforeEach */
|
||||
const simpleAST = require('../__fixtures__/ast-simple.json');
|
||||
const mockVideoAST = require('../__fixtures__/ast-video-challenge.json');
|
||||
// eslint-disable-next-line max-len
|
||||
const videoOutOfOrderAST = require('../__fixtures__/ast-video-out-of-order.json');
|
||||
const addVideoQuestion = require('./add-video-question');
|
||||
|
||||
describe('add-video-question plugin', () => {
|
||||
const plugin = addVideoQuestion();
|
||||
let file = { data: {} };
|
||||
|
||||
beforeEach(() => {
|
||||
file = { data: {} };
|
||||
});
|
||||
|
||||
it('returns a function', () => {
|
||||
expect(typeof plugin).toEqual('function');
|
||||
});
|
||||
|
||||
it('adds a `question` property to `file.data`', () => {
|
||||
plugin(mockVideoAST, file);
|
||||
|
||||
expect('question' in file.data).toBe(true);
|
||||
});
|
||||
|
||||
it('should generate a question object from a video challenge AST', () => {
|
||||
expect.assertions(8);
|
||||
plugin(mockVideoAST, file);
|
||||
const testObject = file.data.question;
|
||||
expect(Object.keys(testObject).length).toBe(3);
|
||||
expect(testObject).toHaveProperty('text');
|
||||
expect(typeof testObject.text).toBe('string');
|
||||
expect(testObject).toHaveProperty('solution');
|
||||
expect(typeof testObject.solution).toBe('number');
|
||||
expect(testObject).toHaveProperty('answers');
|
||||
expect(Array.isArray(testObject.answers)).toBe(true);
|
||||
expect(typeof testObject.answers[0]).toBe('string');
|
||||
});
|
||||
|
||||
it('should convert question and answer markdown into html', () => {
|
||||
plugin(mockVideoAST, file);
|
||||
const testObject = file.data.question;
|
||||
expect(Object.keys(testObject).length).toBe(3);
|
||||
expect(testObject.text).toBe(
|
||||
'<p>Question line 1</p>\n' +
|
||||
`<pre><code class="language-js"> var x = 'y';\n` +
|
||||
'</code></pre>'
|
||||
);
|
||||
expect(testObject.solution).toBe(3);
|
||||
expect(testObject.answers[0]).toBe('<p>Some inline <code>code</code></p>');
|
||||
expect(testObject.answers[1]).toBe(`<p>Some <em>italics</em></p>
|
||||
<p>A second answer paragraph.</p>`);
|
||||
expect(testObject.answers[2]).toBe(
|
||||
'<p><code> code in </code> code tags</p>'
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: consider testing for more specific messages. Ideally we them to say
|
||||
// 'The md is missing "x"', so it's obvious how to fix things.
|
||||
it('should throw if the subheadings are outside the question heading', () => {
|
||||
expect.assertions(1);
|
||||
expect(() => plugin(videoOutOfOrderAST)).toThrow();
|
||||
});
|
||||
|
||||
it('should NOT throw if there is no question', () => {
|
||||
expect.assertions(1);
|
||||
expect(() => plugin(simpleAST)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should match the video snapshot', () => {
|
||||
plugin(mockVideoAST, file);
|
||||
expect(file.data).toMatchSnapshot();
|
||||
});
|
||||
});
|
91
tools/challenge-parser/parser/plugins/replace-imports.js
Normal file
91
tools/challenge-parser/parser/plugins/replace-imports.js
Normal file
@ -0,0 +1,91 @@
|
||||
const path = require('path');
|
||||
const { read } = require('to-vfile');
|
||||
const modifyChildren = require('unist-util-modify-children');
|
||||
const remark = require('remark');
|
||||
const remove = require('unist-util-remove');
|
||||
const visit = require('unist-util-visit');
|
||||
const { selectAll } = require('unist-util-select');
|
||||
const { isEmpty } = require('lodash');
|
||||
|
||||
const { editableRegionMarker } = require('./add-seed');
|
||||
const tableAndStrikeThrough = require('./table-and-strikethrough');
|
||||
|
||||
async function parse(file) {
|
||||
return await remark()
|
||||
.use(tableAndStrikeThrough)
|
||||
.parse(file);
|
||||
}
|
||||
|
||||
function plugin() {
|
||||
return transformer;
|
||||
|
||||
function transformer(tree, file, next) {
|
||||
const importedFiles = selectAll('leafDirective[name=import]', tree);
|
||||
if (!file) {
|
||||
next('replace-imports must be passed a file');
|
||||
return;
|
||||
}
|
||||
if (isEmpty(importedFiles)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const importPromises = importedFiles.map(async ({ attributes }) => {
|
||||
const { from, component } = attributes;
|
||||
const location = path.resolve(file.dirname, from);
|
||||
return await read(location)
|
||||
.then(parse)
|
||||
.then(importedFile => {
|
||||
function modifier(node, index, parent) {
|
||||
const { type, name, attributes } = node;
|
||||
const target = attributes ? attributes.component : null;
|
||||
if (
|
||||
type === 'leafDirective' &&
|
||||
name === 'use' &&
|
||||
target === component
|
||||
) {
|
||||
if (!validateImports(importedFile))
|
||||
throw Error(
|
||||
'Importing files containing ' +
|
||||
editableRegionMarker +
|
||||
's is not supported.'
|
||||
);
|
||||
|
||||
parent.children.splice(index, 1, ...importedFile.children);
|
||||
}
|
||||
}
|
||||
|
||||
const modify = modifyChildren(modifier);
|
||||
modify(tree);
|
||||
});
|
||||
});
|
||||
|
||||
// We're not interested in the results of importing, we just want to
|
||||
// modify the tree and pass that new tree to follow plugins - as a result,
|
||||
// we can't just use .then(next), as it would pass the array into next.
|
||||
// Also, we remove the import statements here.
|
||||
Promise.all(importPromises)
|
||||
.then(() => {
|
||||
remove(tree, { type: 'leafDirective', name: 'import' });
|
||||
next();
|
||||
})
|
||||
.catch(next);
|
||||
}
|
||||
}
|
||||
|
||||
function validateImports(fileTree) {
|
||||
let valid = true;
|
||||
|
||||
function visitor({ value }) {
|
||||
if (value && value.includes(editableRegionMarker)) {
|
||||
valid = false;
|
||||
return visit.EXIT;
|
||||
} else {
|
||||
return visit.CONTINUE;
|
||||
}
|
||||
}
|
||||
|
||||
visit(fileTree, visitor);
|
||||
return valid;
|
||||
}
|
||||
|
||||
module.exports = plugin;
|
231
tools/challenge-parser/parser/plugins/replace-imports.test.js
Normal file
231
tools/challenge-parser/parser/plugins/replace-imports.test.js
Normal file
@ -0,0 +1,231 @@
|
||||
/* global describe it expect */
|
||||
const path = require('path');
|
||||
const cloneDeep = require('lodash/cloneDeep');
|
||||
const toVfile = require('to-vfile');
|
||||
const selectAll = require('unist-util-select').selectAll;
|
||||
|
||||
const addImports = require('./replace-imports');
|
||||
const originalImportsAST = require('../__fixtures__/ast-imports.json');
|
||||
const originalImportsTwoAST = require('../__fixtures__/ast-imports-two.json');
|
||||
const originalSimpleAST = require('../__fixtures__/ast-simple.json');
|
||||
const originalMarkerAST = require('../__fixtures__/ast-marker-imports.json');
|
||||
|
||||
describe('replace-imports', () => {
|
||||
let importsAST;
|
||||
let importsTwoAST;
|
||||
let simpleAST;
|
||||
let markerAST;
|
||||
let correctFile;
|
||||
let incorrectFile;
|
||||
|
||||
beforeEach(() => {
|
||||
importsAST = cloneDeep(originalImportsAST);
|
||||
importsTwoAST = cloneDeep(originalImportsTwoAST);
|
||||
simpleAST = cloneDeep(originalSimpleAST);
|
||||
markerAST = cloneDeep(originalMarkerAST);
|
||||
correctFile = toVfile(
|
||||
path.resolve(__dirname, '../__fixtures__/with-imports.md')
|
||||
);
|
||||
incorrectFile = toVfile(
|
||||
path.resolve(__dirname, '../__fixtures__/incorrect-path/with-imports.md')
|
||||
);
|
||||
});
|
||||
|
||||
it('should return a function', () => {
|
||||
expect.assertions(1);
|
||||
const plugin = addImports();
|
||||
|
||||
expect(typeof plugin).toEqual('function');
|
||||
});
|
||||
|
||||
it('should fail when the imported file is null', done => {
|
||||
const plugin = addImports();
|
||||
const next = err => {
|
||||
if (err) {
|
||||
done();
|
||||
} else {
|
||||
done('An error should have been thrown by addImports');
|
||||
}
|
||||
};
|
||||
plugin(importsAST, null, next);
|
||||
});
|
||||
|
||||
it('should proceed when the imported file exists', done => {
|
||||
const plugin = addImports();
|
||||
plugin(importsAST, correctFile, done);
|
||||
});
|
||||
|
||||
it('should fail when the imported file cannot be found', done => {
|
||||
const plugin = addImports();
|
||||
|
||||
// we have to rely on the next callback, because that is how you get error
|
||||
// messages out of transformers
|
||||
const next = err => {
|
||||
if (err) {
|
||||
done();
|
||||
} else {
|
||||
done('An error should have been thrown by addImports');
|
||||
}
|
||||
};
|
||||
plugin(importsAST, incorrectFile, next);
|
||||
});
|
||||
|
||||
it('should modify the tree when there are imports', done => {
|
||||
expect.assertions(1);
|
||||
const plugin = addImports();
|
||||
const next = err => {
|
||||
if (err) {
|
||||
done(err);
|
||||
} else {
|
||||
expect(importsAST).not.toEqual(originalImportsAST);
|
||||
done();
|
||||
}
|
||||
};
|
||||
plugin(importsAST, correctFile, next);
|
||||
});
|
||||
|
||||
it('should NOT modify the tree when there are NO imports', done => {
|
||||
expect.assertions(1);
|
||||
const plugin = addImports();
|
||||
const next = err => {
|
||||
if (err) {
|
||||
done(err);
|
||||
} else {
|
||||
expect(simpleAST).toEqual(originalSimpleAST);
|
||||
done();
|
||||
}
|
||||
};
|
||||
plugin(simpleAST, correctFile, next);
|
||||
});
|
||||
|
||||
it('should remove all import statements', done => {
|
||||
expect.assertions(2);
|
||||
const selector = 'leafDirective[name=import]';
|
||||
const plugin = addImports();
|
||||
const importNodes = selectAll(selector, importsAST);
|
||||
|
||||
expect(importNodes.length).toBe(1);
|
||||
|
||||
const next = err => {
|
||||
if (err) {
|
||||
done(err);
|
||||
} else {
|
||||
const importNodes = selectAll(selector, importsAST);
|
||||
expect(importNodes.length).toBe(0);
|
||||
done();
|
||||
}
|
||||
};
|
||||
plugin(importsAST, correctFile, next);
|
||||
});
|
||||
|
||||
it('should remove all matching ::use statements', done => {
|
||||
expect.assertions(2);
|
||||
const selector = 'leafDirective[name=use]';
|
||||
const plugin = addImports();
|
||||
const components = selectAll(selector, importsAST);
|
||||
|
||||
// one matching component and two other jsx nodes
|
||||
expect(components.length).toBe(1);
|
||||
|
||||
const next = err => {
|
||||
if (err) {
|
||||
done(err);
|
||||
} else {
|
||||
const components = selectAll(selector, importsAST);
|
||||
expect(components.length).toBe(0);
|
||||
done();
|
||||
}
|
||||
};
|
||||
plugin(importsAST, correctFile, next);
|
||||
});
|
||||
|
||||
it('should replace the ::use statement with the imported content', done => {
|
||||
// checks the contents of script.md are there after the import step
|
||||
expect.assertions(2);
|
||||
const plugin = addImports();
|
||||
|
||||
const next = err => {
|
||||
if (err) {
|
||||
done(err);
|
||||
} else {
|
||||
const jsNodes = selectAll('code[lang=js]', importsAST);
|
||||
expect(jsNodes.length).toBe(4);
|
||||
|
||||
const codeValues = jsNodes.map(({ value }) => value);
|
||||
expect(codeValues).toEqual(
|
||||
expect.arrayContaining([
|
||||
`for (let index = 0; index < array.length; index++) {
|
||||
const element = array[index];
|
||||
// imported from script.md
|
||||
}`
|
||||
])
|
||||
);
|
||||
done();
|
||||
}
|
||||
};
|
||||
plugin(importsAST, correctFile, next);
|
||||
});
|
||||
|
||||
it('should handle multiple import statements', done => {
|
||||
// checks the contents of script.md are there after the import step
|
||||
expect.assertions(4);
|
||||
const plugin = addImports();
|
||||
|
||||
const next = err => {
|
||||
if (err) {
|
||||
done(err);
|
||||
} else {
|
||||
const jsNodes = selectAll('code[lang=js]', importsTwoAST);
|
||||
expect(jsNodes.length).toBe(4);
|
||||
|
||||
const codeValues = jsNodes.map(({ value }) => value);
|
||||
expect(codeValues).toEqual(
|
||||
expect.arrayContaining([
|
||||
`for (let index = 0; index < array.length; index++) {
|
||||
const element = array[index];
|
||||
// imported from script.md
|
||||
}`
|
||||
])
|
||||
);
|
||||
const cssNodes = selectAll('code[lang=css]', importsTwoAST);
|
||||
expect(cssNodes.length).toBe(2);
|
||||
|
||||
const cssValues = cssNodes.map(({ value }) => value);
|
||||
expect(cssValues).toEqual(
|
||||
expect.arrayContaining([
|
||||
`div {
|
||||
background: red
|
||||
}`
|
||||
])
|
||||
);
|
||||
done();
|
||||
}
|
||||
};
|
||||
plugin(importsTwoAST, correctFile, next);
|
||||
});
|
||||
|
||||
it('should reject imported files with editable region markers', done => {
|
||||
const plugin = addImports();
|
||||
const next = err => {
|
||||
if (err) {
|
||||
done();
|
||||
} else {
|
||||
done('An error should have been thrown by addImports');
|
||||
}
|
||||
};
|
||||
plugin(markerAST, correctFile, next);
|
||||
});
|
||||
|
||||
it('should have an output to match the snapshot', done => {
|
||||
const plugin = addImports();
|
||||
const next = err => {
|
||||
if (err) {
|
||||
done(err);
|
||||
} else {
|
||||
expect(importsAST).toMatchSnapshot();
|
||||
done();
|
||||
}
|
||||
};
|
||||
plugin(importsAST, correctFile, next);
|
||||
});
|
||||
});
|
@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
var strikethrough = require('micromark-extension-gfm-strikethrough');
|
||||
var table = require('micromark-extension-gfm-table');
|
||||
var fromMarkdown = require('mdast-util-gfm/from-markdown');
|
||||
|
||||
module.exports = tableAndStrikethrough;
|
||||
|
||||
function tableAndStrikethrough() {
|
||||
var data = this.data();
|
||||
|
||||
add('micromarkExtensions', strikethrough());
|
||||
add('micromarkExtensions', table);
|
||||
add('fromMarkdownExtensions', fromMarkdown);
|
||||
|
||||
function add(field, value) {
|
||||
if (data[field]) data[field].push(value);
|
||||
else data[field] = [value];
|
||||
}
|
||||
}
|
||||
|
||||
// Based on remark-gfm, extended as described in
|
||||
// https://github.com/remarkjs/remark/tree/main/packages/remark-parse#extending-the-parser
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"type": "leafDirective",
|
||||
"name": "id",
|
||||
"attributes": { "id": "html-key" },
|
||||
"children": [],
|
||||
"position": {
|
||||
"start": { "line": 29, "column": 1, "offset": 200 },
|
||||
"end": { "line": 29, "column": 16, "offset": 215 }
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
{
|
||||
"type": "paragraph",
|
||||
"children": [
|
||||
{
|
||||
"type": "image",
|
||||
"title": null,
|
||||
"url": "https://www.image.com",
|
||||
"alt": "html-key",
|
||||
"position": {
|
||||
"start": { "line": 65, "column": 1, "offset": 481 },
|
||||
"end": { "line": 65, "column": 35, "offset": 515 },
|
||||
"indent": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"position": {
|
||||
"start": { "line": 65, "column": 1, "offset": 481 },
|
||||
"end": { "line": 65, "column": 35, "offset": 515 },
|
||||
"indent": []
|
||||
}
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
{
|
||||
"type": "paragraph",
|
||||
"children": [
|
||||
{
|
||||
"type": "html",
|
||||
"value": "<code>",
|
||||
"position": {
|
||||
"start": { "line": 3, "column": 1, "offset": 19 },
|
||||
"end": { "line": 3, "column": 7, "offset": 25 },
|
||||
"indent": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"value": " code in ",
|
||||
"position": {
|
||||
"start": { "line": 3, "column": 7, "offset": 25 },
|
||||
"end": { "line": 3, "column": 16, "offset": 34 },
|
||||
"indent": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "html",
|
||||
"value": "</code>",
|
||||
"position": {
|
||||
"start": { "line": 3, "column": 16, "offset": 34 },
|
||||
"end": { "line": 3, "column": 23, "offset": 41 },
|
||||
"indent": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"value": " code tags ",
|
||||
"position": {
|
||||
"start": { "line": 3, "column": 23, "offset": 41 },
|
||||
"end": { "line": 3, "column": 34, "offset": 52 },
|
||||
"indent": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "emphasis",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"value": "emphasis",
|
||||
"position": {
|
||||
"start": { "line": 3, "column": 35, "offset": 53 },
|
||||
"end": { "line": 3, "column": 43, "offset": 61 },
|
||||
"indent": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"position": {
|
||||
"start": { "line": 3, "column": 34, "offset": 52 },
|
||||
"end": { "line": 3, "column": 44, "offset": 62 },
|
||||
"indent": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"value": " followed by ",
|
||||
"position": {
|
||||
"start": { "line": 3, "column": 44, "offset": 62 },
|
||||
"end": { "line": 3, "column": 57, "offset": 75 },
|
||||
"indent": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "html",
|
||||
"value": "<div>",
|
||||
"position": {
|
||||
"start": { "line": 3, "column": 57, "offset": 75 },
|
||||
"end": { "line": 3, "column": 62, "offset": 80 },
|
||||
"indent": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "html",
|
||||
"value": "<span>",
|
||||
"position": {
|
||||
"start": { "line": 3, "column": 62, "offset": 80 },
|
||||
"end": { "line": 3, "column": 68, "offset": 86 },
|
||||
"indent": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"value": "some nested html ",
|
||||
"position": {
|
||||
"start": { "line": 3, "column": 68, "offset": 86 },
|
||||
"end": { "line": 3, "column": 85, "offset": 103 },
|
||||
"indent": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "html",
|
||||
"value": "</span>",
|
||||
"position": {
|
||||
"start": { "line": 3, "column": 85, "offset": 103 },
|
||||
"end": { "line": 3, "column": 92, "offset": 110 },
|
||||
"indent": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "html",
|
||||
"value": "</div>",
|
||||
"position": {
|
||||
"start": { "line": 3, "column": 92, "offset": 110 },
|
||||
"end": { "line": 3, "column": 98, "offset": 116 },
|
||||
"indent": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"position": {
|
||||
"start": { "line": 3, "column": 1, "offset": 19 },
|
||||
"end": { "line": 3, "column": 98, "offset": 116 },
|
||||
"indent": []
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
[
|
||||
{
|
||||
"type": "paragraph",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"value": "Paragraph 1",
|
||||
"position": {
|
||||
"start": { "line": 3, "column": 1, "offset": 19 },
|
||||
"end": { "line": 3, "column": 12, "offset": 30 },
|
||||
"indent": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"position": {
|
||||
"start": { "line": 3, "column": 1, "offset": 19 },
|
||||
"end": { "line": 3, "column": 12, "offset": 30 },
|
||||
"indent": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"value": "Third ",
|
||||
"position": {
|
||||
"start": { "line": 5, "column": 1, "offset": 32 },
|
||||
"end": { "line": 5, "column": 7, "offset": 38 },
|
||||
"indent": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "emphasis",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"value": "hint",
|
||||
"position": {
|
||||
"start": { "line": 5, "column": 8, "offset": 39 },
|
||||
"end": { "line": 5, "column": 12, "offset": 43 },
|
||||
"indent": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"position": {
|
||||
"start": { "line": 5, "column": 7, "offset": 38 },
|
||||
"end": { "line": 5, "column": 13, "offset": 44 },
|
||||
"indent": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"value": " with ",
|
||||
"position": {
|
||||
"start": { "line": 5, "column": 13, "offset": 44 },
|
||||
"end": { "line": 5, "column": 19, "offset": 50 },
|
||||
"indent": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "html",
|
||||
"value": "<code>",
|
||||
"position": {
|
||||
"start": { "line": 5, "column": 19, "offset": 50 },
|
||||
"end": { "line": 5, "column": 25, "offset": 56 },
|
||||
"indent": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"value": "code",
|
||||
"position": {
|
||||
"start": { "line": 5, "column": 25, "offset": 56 },
|
||||
"end": { "line": 5, "column": 29, "offset": 60 },
|
||||
"indent": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "html",
|
||||
"value": "</code>",
|
||||
"position": {
|
||||
"start": { "line": 5, "column": 29, "offset": 60 },
|
||||
"end": { "line": 5, "column": 36, "offset": 67 },
|
||||
"indent": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"value": " and ",
|
||||
"position": {
|
||||
"start": { "line": 5, "column": 36, "offset": 67 },
|
||||
"end": { "line": 5, "column": 41, "offset": 72 },
|
||||
"indent": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "inlineCode",
|
||||
"value": "inline code",
|
||||
"position": {
|
||||
"start": { "line": 5, "column": 41, "offset": 72 },
|
||||
"end": { "line": 5, "column": 54, "offset": 85 },
|
||||
"indent": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"position": {
|
||||
"start": { "line": 5, "column": 1, "offset": 32 },
|
||||
"end": { "line": 5, "column": 54, "offset": 85 },
|
||||
"indent": []
|
||||
}
|
||||
}
|
||||
]
|
@ -0,0 +1,66 @@
|
||||
{
|
||||
"type": "paragraph",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"value": "Just some ",
|
||||
"position": {
|
||||
"start": { "line": 11, "column": 1, "offset": 120 },
|
||||
"end": { "line": 11, "column": 11, "offset": 130 },
|
||||
"indent": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "emphasis",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"value": "emphasis",
|
||||
"position": {
|
||||
"start": { "line": 11, "column": 12, "offset": 131 },
|
||||
"end": { "line": 11, "column": 20, "offset": 139 },
|
||||
"indent": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"position": {
|
||||
"start": { "line": 11, "column": 11, "offset": 130 },
|
||||
"end": { "line": 11, "column": 21, "offset": 140 },
|
||||
"indent": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"value": " and a bit of ",
|
||||
"position": {
|
||||
"start": { "line": 11, "column": 21, "offset": 140 },
|
||||
"end": { "line": 11, "column": 35, "offset": 154 },
|
||||
"indent": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "strong",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"value": "bold",
|
||||
"position": {
|
||||
"start": { "line": 11, "column": 37, "offset": 156 },
|
||||
"end": { "line": 11, "column": 41, "offset": 160 },
|
||||
"indent": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"position": {
|
||||
"start": { "line": 11, "column": 35, "offset": 154 },
|
||||
"end": { "line": 11, "column": 43, "offset": 162 },
|
||||
"indent": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"position": {
|
||||
"start": { "line": 11, "column": 1, "offset": 120 },
|
||||
"end": { "line": 11, "column": 43, "offset": 162 },
|
||||
"indent": []
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
{
|
||||
"type": "paragraph",
|
||||
"children": [
|
||||
{
|
||||
"type": "imageReference",
|
||||
"identifier": "html-key",
|
||||
"label": "html-key",
|
||||
"referenceType": "shortcut",
|
||||
"alt": "html-key",
|
||||
"position": {
|
||||
"start": { "line": 65, "column": 1, "offset": 481 },
|
||||
"end": { "line": 65, "column": 12, "offset": 492 },
|
||||
"indent": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"value": "\nMore stuff in the paragraph",
|
||||
"position": {
|
||||
"start": { "line": 65, "column": 12, "offset": 492 },
|
||||
"end": { "line": 66, "column": 28, "offset": 520 },
|
||||
"indent": [1]
|
||||
}
|
||||
}
|
||||
],
|
||||
"position": {
|
||||
"start": { "line": 65, "column": 1, "offset": 481 },
|
||||
"end": { "line": 66, "column": 28, "offset": 520 },
|
||||
"indent": [1]
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
{
|
||||
"type": "paragraph",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"value": "--description--",
|
||||
"position": {
|
||||
"start": { "line": 65, "column": 1, "offset": 481 },
|
||||
"end": { "line": 65, "column": 12, "offset": 492 },
|
||||
"indent": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"position": {
|
||||
"start": { "line": 65, "column": 1, "offset": 481 },
|
||||
"end": { "line": 65, "column": 12, "offset": 492 },
|
||||
"indent": []
|
||||
}
|
||||
}
|
@ -0,0 +1,395 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`between-headings should match the hints snapshot 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 11,
|
||||
"line": 19,
|
||||
"offset": 142,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 19,
|
||||
"offset": 132,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "First hint",
|
||||
},
|
||||
],
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 11,
|
||||
"line": 19,
|
||||
"offset": 142,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 19,
|
||||
"offset": 132,
|
||||
},
|
||||
},
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"lang": "js",
|
||||
"meta": null,
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 4,
|
||||
"line": 23,
|
||||
"offset": 166,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 21,
|
||||
"offset": 144,
|
||||
},
|
||||
},
|
||||
"type": "code",
|
||||
"value": "// test code",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 18,
|
||||
"line": 25,
|
||||
"offset": 185,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 25,
|
||||
"offset": 168,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "Second hint with ",
|
||||
},
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 24,
|
||||
"line": 25,
|
||||
"offset": 191,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 18,
|
||||
"line": 25,
|
||||
"offset": 185,
|
||||
},
|
||||
},
|
||||
"type": "html",
|
||||
"value": "<code>",
|
||||
},
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 28,
|
||||
"line": 25,
|
||||
"offset": 195,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 24,
|
||||
"line": 25,
|
||||
"offset": 191,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "code",
|
||||
},
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 35,
|
||||
"line": 25,
|
||||
"offset": 202,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 28,
|
||||
"line": 25,
|
||||
"offset": 195,
|
||||
},
|
||||
},
|
||||
"type": "html",
|
||||
"value": "</code>",
|
||||
},
|
||||
],
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 35,
|
||||
"line": 25,
|
||||
"offset": 202,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 25,
|
||||
"offset": 168,
|
||||
},
|
||||
},
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"lang": "js",
|
||||
"meta": null,
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 4,
|
||||
"line": 29,
|
||||
"offset": 231,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 27,
|
||||
"offset": 204,
|
||||
},
|
||||
},
|
||||
"type": "code",
|
||||
"value": "// more test code",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 7,
|
||||
"line": 31,
|
||||
"offset": 239,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 31,
|
||||
"offset": 233,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "Third ",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 12,
|
||||
"line": 31,
|
||||
"offset": 244,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 8,
|
||||
"line": 31,
|
||||
"offset": 240,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "hint",
|
||||
},
|
||||
],
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 13,
|
||||
"line": 31,
|
||||
"offset": 245,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 7,
|
||||
"line": 31,
|
||||
"offset": 239,
|
||||
},
|
||||
},
|
||||
"type": "emphasis",
|
||||
},
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 19,
|
||||
"line": 31,
|
||||
"offset": 251,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 13,
|
||||
"line": 31,
|
||||
"offset": 245,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": " with ",
|
||||
},
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 25,
|
||||
"line": 31,
|
||||
"offset": 257,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 19,
|
||||
"line": 31,
|
||||
"offset": 251,
|
||||
},
|
||||
},
|
||||
"type": "html",
|
||||
"value": "<code>",
|
||||
},
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 29,
|
||||
"line": 31,
|
||||
"offset": 261,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 25,
|
||||
"line": 31,
|
||||
"offset": 257,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "code",
|
||||
},
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 36,
|
||||
"line": 31,
|
||||
"offset": 268,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 29,
|
||||
"line": 31,
|
||||
"offset": 261,
|
||||
},
|
||||
},
|
||||
"type": "html",
|
||||
"value": "</code>",
|
||||
},
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 41,
|
||||
"line": 31,
|
||||
"offset": 273,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 36,
|
||||
"line": 31,
|
||||
"offset": 268,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": " and ",
|
||||
},
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 54,
|
||||
"line": 31,
|
||||
"offset": 286,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 41,
|
||||
"line": 31,
|
||||
"offset": 273,
|
||||
},
|
||||
},
|
||||
"type": "inlineCode",
|
||||
"value": "inline code",
|
||||
},
|
||||
],
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 54,
|
||||
"line": 31,
|
||||
"offset": 286,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 31,
|
||||
"offset": 233,
|
||||
},
|
||||
},
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"lang": "js",
|
||||
"meta": null,
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 4,
|
||||
"line": 38,
|
||||
"offset": 353,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 33,
|
||||
"offset": 288,
|
||||
},
|
||||
},
|
||||
"type": "code",
|
||||
"value": "// more test code
|
||||
if(let x of xs) {
|
||||
console.log(x);
|
||||
}",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`between-headings should match the instructions snapshot 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 12,
|
||||
"line": 11,
|
||||
"offset": 89,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 11,
|
||||
"offset": 78,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "Paragraph 0",
|
||||
},
|
||||
],
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 12,
|
||||
"line": 11,
|
||||
"offset": 89,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 11,
|
||||
"offset": 78,
|
||||
},
|
||||
},
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"lang": "html",
|
||||
"meta": null,
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 4,
|
||||
"line": 15,
|
||||
"offset": 117,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 13,
|
||||
"offset": 91,
|
||||
},
|
||||
},
|
||||
"type": "code",
|
||||
"value": "code example 0",
|
||||
},
|
||||
]
|
||||
`;
|
@ -0,0 +1,46 @@
|
||||
const between = require('unist-util-find-all-between');
|
||||
const find = require('unist-util-find');
|
||||
const findAfter = require('unist-util-find-after');
|
||||
const findAllAfter = require('unist-util-find-all-after');
|
||||
|
||||
function getAllBetween(tree, marker) {
|
||||
const start = find(tree, {
|
||||
type: 'heading',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: marker
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!start) return [];
|
||||
|
||||
const isEnd = node => {
|
||||
return (
|
||||
node.type === 'heading' && node.depth <= start.depth && isMarker(node)
|
||||
);
|
||||
};
|
||||
|
||||
const isMarker = node => {
|
||||
if (node.children && node.children[0]) {
|
||||
const child = node.children[0];
|
||||
return (
|
||||
child.type === 'text' &&
|
||||
child.value.startsWith('--') &&
|
||||
child.value.endsWith('--')
|
||||
);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const end = findAfter(tree, start, isEnd);
|
||||
|
||||
const targetNodes = end
|
||||
? between(tree, start, end)
|
||||
: findAllAfter(tree, start);
|
||||
return targetNodes;
|
||||
}
|
||||
|
||||
module.exports = getAllBetween;
|
@ -0,0 +1,51 @@
|
||||
/* global expect*/
|
||||
const isArray = require('lodash/isArray');
|
||||
const find = require('unist-util-find');
|
||||
const { root } = require('mdast-builder');
|
||||
|
||||
const getAllBetween = require('./between-headings');
|
||||
const simpleAst = require('../../__fixtures__/ast-simple.json');
|
||||
const extraHeadingAst = require('../../__fixtures__/ast-extra-heading.json');
|
||||
|
||||
describe('between-headings', () => {
|
||||
it('should return an array', () => {
|
||||
expect.assertions(1);
|
||||
const actual = getAllBetween(simpleAst, '--hints--');
|
||||
expect(isArray(actual)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return an empty array if the marker is not present', () => {
|
||||
expect.assertions(2);
|
||||
const actual = getAllBetween(simpleAst, '--not-a-marker--');
|
||||
expect(isArray(actual)).toBe(true);
|
||||
expect(actual.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should include any headings without markers', () => {
|
||||
expect.assertions(1);
|
||||
const actual = getAllBetween(extraHeadingAst, '--description--');
|
||||
expect(
|
||||
find(root(actual), {
|
||||
value: 'this should still be inside --description--'
|
||||
})
|
||||
).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should include the rest of the AST if there is no end marker', () => {
|
||||
expect.assertions(2);
|
||||
const actual = getAllBetween(extraHeadingAst, '--solutions--');
|
||||
expect(actual.length > 0).toBe(true);
|
||||
expect(
|
||||
find(root(actual), { value: 'body {\n background: white;\n}' })
|
||||
).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should match the hints snapshot', () => {
|
||||
const actual = getAllBetween(simpleAst, '--hints--');
|
||||
expect(actual).toMatchSnapshot();
|
||||
});
|
||||
it('should match the instructions snapshot', () => {
|
||||
const actual = getAllBetween(simpleAst, '--instructions--');
|
||||
expect(actual).toMatchSnapshot();
|
||||
});
|
||||
});
|
@ -0,0 +1,84 @@
|
||||
const is = require('unist-util-is');
|
||||
const position = require('unist-util-position');
|
||||
const { isEmpty } = require('lodash');
|
||||
|
||||
const getId = require('./get-id');
|
||||
|
||||
const keyToSection = {
|
||||
head: 'before-user-code',
|
||||
tail: 'after-user-code'
|
||||
};
|
||||
const supportedLanguages = ['js', 'css', 'html', 'jsx', 'py'];
|
||||
|
||||
function defaultFile(lang, id) {
|
||||
return {
|
||||
key: `index${lang}`,
|
||||
ext: lang,
|
||||
name: 'index',
|
||||
contents: '',
|
||||
head: '',
|
||||
tail: '',
|
||||
id
|
||||
};
|
||||
}
|
||||
|
||||
function getFileVisitor(seeds, seedKey, validate) {
|
||||
return (node, index, parent) => {
|
||||
if (is(node, 'root')) return;
|
||||
if (is(node, 'code')) {
|
||||
codeToData(node, seeds, seedKey, validate);
|
||||
return;
|
||||
}
|
||||
idToData(node, index, parent, seeds);
|
||||
};
|
||||
}
|
||||
|
||||
function codeToData(node, seeds, seedKey, validate) {
|
||||
if (validate) validate(node);
|
||||
const lang = node.lang;
|
||||
if (!supportedLanguages.includes(lang))
|
||||
throw Error(`On line ${
|
||||
position.start(node).line
|
||||
} '${lang}' is not a supported language.
|
||||
Please use one of js, css, html, jsx or py
|
||||
`);
|
||||
|
||||
const key = `index${lang}`;
|
||||
const id = seeds[key] ? seeds[key].id : '';
|
||||
// the contents will be missing if there is an id preceding this code
|
||||
// block.
|
||||
if (!seeds[key]) {
|
||||
seeds[key] = defaultFile(lang, id);
|
||||
}
|
||||
if (isEmpty(node.value) && seedKey !== 'contents') {
|
||||
const section = keyToSection[seedKey];
|
||||
throw Error(`Empty code block in --${section}-- section`);
|
||||
}
|
||||
|
||||
seeds[key][seedKey] = isEmpty(seeds[key][seedKey])
|
||||
? node.value
|
||||
: seeds[key][seedKey] + '\n' + node.value;
|
||||
}
|
||||
|
||||
function idToData(node, index, parent, seeds) {
|
||||
const id = getId(node);
|
||||
|
||||
// If this is reached, the node type is neither root nor code. If it is not
|
||||
// an id, there must be a syntax error.
|
||||
if (!id) {
|
||||
throw Error(
|
||||
'Unexpected syntax in seed/solution. Must be ::id{#id} or a code ' +
|
||||
'block (```) \n'
|
||||
);
|
||||
}
|
||||
const codeNode = parent.children[index + 1];
|
||||
if (codeNode && is(codeNode, 'code')) {
|
||||
const key = `index${codeNode.lang}`;
|
||||
if (seeds[key]) throw Error('::id{#id}s must come before code blocks');
|
||||
seeds[key] = defaultFile(codeNode.lang, id);
|
||||
} else {
|
||||
throw Error('::id{#id}s must come before code blocks');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.getFileVisitor = getFileVisitor;
|
@ -0,0 +1,33 @@
|
||||
describe('get-file-visitor', () => {
|
||||
it('should join code with newlines', () => {
|
||||
/* i.e. if you've got two js code blocks it should do this
|
||||
|
||||
```js
|
||||
one
|
||||
```
|
||||
|
||||
```js
|
||||
two
|
||||
```
|
||||
|
||||
become
|
||||
|
||||
```js
|
||||
one
|
||||
two
|
||||
```
|
||||
|
||||
not
|
||||
|
||||
```js
|
||||
onetwo
|
||||
```
|
||||
or
|
||||
```js
|
||||
|
||||
one
|
||||
two
|
||||
```
|
||||
*/
|
||||
});
|
||||
});
|
8
tools/challenge-parser/parser/plugins/utils/get-id.js
Normal file
8
tools/challenge-parser/parser/plugins/utils/get-id.js
Normal file
@ -0,0 +1,8 @@
|
||||
// getId expects the image reference node to be the sole node in a paragraph
|
||||
function getId(node) {
|
||||
const { type, name, attributes } = node;
|
||||
if (type !== 'leafDirective' || name !== 'id' || !attributes) return null;
|
||||
return attributes.id;
|
||||
}
|
||||
|
||||
module.exports = getId;
|
45
tools/challenge-parser/parser/plugins/utils/get-id.test.js
Normal file
45
tools/challenge-parser/parser/plugins/utils/get-id.test.js
Normal file
@ -0,0 +1,45 @@
|
||||
/* global expect*/
|
||||
const getId = require('./get-id');
|
||||
const idNode = require('./__fixtures__/id-node.json');
|
||||
const imageNode = require('./__fixtures__/image-node.json');
|
||||
const multipleChildrenNode = require('./__fixtures__/multiple-children.json');
|
||||
const nonIdNode = require('./__fixtures__/non-id-node.json');
|
||||
|
||||
describe('get-id', () => {
|
||||
it('should return a string', () => {
|
||||
expect.assertions(1);
|
||||
const actual = getId(idNode);
|
||||
expect(typeof actual).toBe('string');
|
||||
});
|
||||
|
||||
it('should get the expected identifier', () => {
|
||||
expect.assertions(1);
|
||||
const actual = getId(idNode);
|
||||
expect(actual).toBe('html-key');
|
||||
});
|
||||
|
||||
it('should return null if the node does contain an id', () => {
|
||||
expect.assertions(1);
|
||||
const actual = getId(nonIdNode);
|
||||
expect(actual).toBeNull();
|
||||
});
|
||||
|
||||
// TODO: bin this (and the json!) after development (it'll be a silly test
|
||||
// once we're using directives)
|
||||
it('should ignore image nodes', () => {
|
||||
expect.assertions(1);
|
||||
const actual = getId(imageNode);
|
||||
expect(actual).toBeNull();
|
||||
});
|
||||
|
||||
// TODO: bin this (and the json!) after development (it'll be a silly test
|
||||
// once we're using directives)
|
||||
|
||||
// TODO: do we want to fail silently? Might it be better to output warnings
|
||||
// or perhaps even stop the parser? Probably warnings if anything.
|
||||
it('should ignore paragraphs that contain more than the id element', () => {
|
||||
expect.assertions(1);
|
||||
const actual = getId(multipleChildrenNode);
|
||||
expect(actual).toBeNull();
|
||||
});
|
||||
});
|
15
tools/challenge-parser/parser/plugins/utils/mdast-to-html.js
Normal file
15
tools/challenge-parser/parser/plugins/utils/mdast-to-html.js
Normal file
@ -0,0 +1,15 @@
|
||||
const hastToHTML = require('hast-util-to-html');
|
||||
const mdastToHast = require('mdast-util-to-hast');
|
||||
const { root } = require('mdast-builder');
|
||||
|
||||
function mdastToHTML(nodes) {
|
||||
if (!Array.isArray(nodes))
|
||||
throw Error('mdastToHTML expects an array argument');
|
||||
// - the 'nodes' are children, so first need embedding in a parent
|
||||
|
||||
return hastToHTML(mdastToHast(root(nodes), { allowDangerousHtml: true }), {
|
||||
allowDangerousHtml: true
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = mdastToHTML;
|
@ -0,0 +1,42 @@
|
||||
/* global expect*/
|
||||
const mdastToHTML = require('./mdast-to-html');
|
||||
const mdastMixedNodes = require('./__fixtures__/mdast-mixed-nodes.json');
|
||||
const mdastWithEmNode = require('./__fixtures__/mdast-with-em.json');
|
||||
const singleNode = require('./__fixtures__/non-id-node.json');
|
||||
const leadingInlineHTMLNode = require('./__fixtures__/leading-html-node.json');
|
||||
|
||||
describe('mdast-to-html', () => {
|
||||
it('should return a string', () => {
|
||||
expect.assertions(1);
|
||||
const actual = mdastToHTML(mdastMixedNodes);
|
||||
expect(typeof actual).toBe('string');
|
||||
});
|
||||
|
||||
it('throws if it is not passed an array', () => {
|
||||
expect.assertions(1);
|
||||
expect(() => mdastToHTML(singleNode)).toThrow(
|
||||
'mdastToHTML expects an array argument'
|
||||
);
|
||||
});
|
||||
|
||||
it('should convert markdown nodes into html', () => {
|
||||
const actual = mdastToHTML([mdastWithEmNode]);
|
||||
expect(actual).toBe(
|
||||
'<p>Just some <em>emphasis</em> and a bit of <strong>bold</strong></p>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not escape html', () => {
|
||||
const actual = mdastToHTML(mdastMixedNodes);
|
||||
expect(actual).toBe(`<p>Paragraph 1</p>
|
||||
<p>Third <em>hint</em> with <code>code</code> and <code>inline code</code></p>`);
|
||||
});
|
||||
|
||||
it('should put inline html inside the enclosing paragraph', () => {
|
||||
const actual = mdastToHTML([leadingInlineHTMLNode]);
|
||||
expect(actual).toBe(
|
||||
'<p><code> code in </code> code tags <em>emphasis</em> followed' +
|
||||
' by <div><span>some nested html </span></div></p>'
|
||||
);
|
||||
});
|
||||
});
|
@ -0,0 +1,20 @@
|
||||
const is = require('unist-util-is');
|
||||
|
||||
// TODO: specific tests for this would be nice, even though it is somewhat
|
||||
// covered by the plugins that use it.
|
||||
function splitOnThematicBreak(nodes) {
|
||||
return nodes.reduce(
|
||||
(prev, curr) => {
|
||||
if (is(curr, 'thematicBreak')) {
|
||||
return [...prev, []];
|
||||
} else {
|
||||
const first = prev.slice(0, -1);
|
||||
const last = prev.slice(-1)[0];
|
||||
return [...first, [...last, curr]];
|
||||
}
|
||||
},
|
||||
[[]]
|
||||
);
|
||||
}
|
||||
|
||||
exports.splitOnThematicBreak = splitOnThematicBreak;
|
Reference in New Issue
Block a user