chore: remove old parser

This commit is contained in:
Oliver Eyton-Williams
2021-02-01 19:31:39 +01:00
committed by Mrugesh Mohapatra
parent e3511f2930
commit a3a678b7af
139 changed files with 4 additions and 4 deletions

View File

@ -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",
}
`;

View File

@ -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": "",
},
},
}
`;

View File

@ -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": "",
},
},
],
}
`;

View File

@ -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>",
},
],
}
`;

View File

@ -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>",
}
`;

View File

@ -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>",
},
}
`;

View File

@ -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",
}
`;

View File

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

View File

@ -0,0 +1,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();
});
});

View 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;

View 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;

View 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();
});
});

View 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;

View 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();
});
});

View 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;

View 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();
});
});

View 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;

View 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();
});
});

View 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;

View File

@ -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();
});
});

View 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;

View 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);
});
});

View File

@ -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

View File

@ -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 }
}
}

View File

@ -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": []
}
}

View File

@ -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": []
}
}

View File

@ -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": []
}
}
]

View File

@ -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": []
}
}

View File

@ -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]
}
}

View File

@ -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": []
}
}

View File

@ -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",
},
]
`;

View File

@ -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;

View File

@ -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();
});
});

View File

@ -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;

View File

@ -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
```
*/
});
});

View 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;

View 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();
});
});

View 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;

View File

@ -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>'
);
});
});

View File

@ -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;