diff --git a/tools/challenge-md-parser/__snapshots__/challengeSeed-to-data.test.js.snap b/tools/challenge-md-parser/__snapshots__/challengeSeed-to-data.test.js.snap new file mode 100644 index 0000000000..4076256e78 --- /dev/null +++ b/tools/challenge-md-parser/__snapshots__/challengeSeed-to-data.test.js.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`challengeSeed-to-data plugin should have an output to match the snapshot 1`] = ` +Object { + "files": Array [ + Object { + "contents": "function testFunction(arg) { + return arg; +} + +testFunction('hello'); +", + "ext": "js", + "head": "console.log('before the test'); +", + "key": "indexjs", + "name": "index", + "tail": "console.info('after the test'); +", + }, + ], +} +`; diff --git a/tools/challenge-md-parser/challengeSeed-to-data.js b/tools/challenge-md-parser/challengeSeed-to-data.js new file mode 100644 index 0000000000..08302c46ef --- /dev/null +++ b/tools/challenge-md-parser/challengeSeed-to-data.js @@ -0,0 +1,72 @@ +const visit = require('unist-util-visit'); +const { selectAll, select } = require('hast-util-select'); + +const { sectionFilter } = require('./utils'); + +const seedRE = /(.+)-seed$/; +const headRE = /(.+)-setup$/; +const tailRE = /(.+)-teardown$/; + +function defaultFile(lang) { + return { + key: `index${lang}`, + ext: lang, + name: 'index', + contents: '', + head: '', + tail: '' + }; +} +function createCodeGetter(key, regEx, seeds) { + return container => { + const { + properties: { id } + } = container; + const lang = id.match(regEx)[1]; + const code = select('code', container).children[0].value; + if (lang in seeds) { + seeds[lang] = { + ...seeds[lang], + [key]: code + }; + } else { + seeds[lang] = { + ...defaultFile(lang), + [key]: code + }; + } + }; +} + +function createPlugin() { + return function transformer(tree, file) { + function visitor(node) { + if (sectionFilter(node, 'challengeSeed')) { + let seeds = {}; + const codeDivs = selectAll('div', node); + const seedConatiners = codeDivs.filter(({ properties: { id } }) => + seedRE.test(id) + ); + seedConatiners.forEach(createCodeGetter('contents', seedRE, seeds)); + + const headConatiners = codeDivs.filter(({ properties: { id } }) => + headRE.test(id) + ); + headConatiners.forEach(createCodeGetter('head', headRE, seeds)); + + const tailConatiners = codeDivs.filter(({ properties: { id } }) => + tailRE.test(id) + ); + tailConatiners.forEach(createCodeGetter('tail', tailRE, seeds)); + + file.data = { + ...file.data, + files: Object.keys(seeds).map(lang => seeds[lang]) + }; + } + } + visit(tree, 'element', visitor); + }; +} + +module.exports = createPlugin; diff --git a/tools/challenge-md-parser/challengeSeed-to-data.test.js b/tools/challenge-md-parser/challengeSeed-to-data.test.js new file mode 100644 index 0000000000..449256965b --- /dev/null +++ b/tools/challenge-md-parser/challengeSeed-to-data.test.js @@ -0,0 +1,59 @@ +/* global describe it expect beforeEach */ +const mockAST = require('./fixtures/challenge-html-ast.json'); +const challengeSeedToData = require('./challengeSeed-to-data'); + +describe('challengeSeed-to-data plugin', () => { + const plugin = challengeSeedToData(); + let file = { data: {} }; + + beforeEach(() => { + file = { data: {} }; + }); + + it('returns a function', () => { + expect(typeof plugin).toEqual('function'); + }); + + it('adds a `files` property to `file.data`', () => { + plugin(mockAST, file); + expect('files' in file.data).toBe(true); + }); + + it('ensures that the `files` property is an array', () => { + plugin(mockAST, file); + expect(Array.isArray(file.data.files)).toBe(true); + }); + + it('adds test objects to the files array following a schema', () => { + expect.assertions(7); + plugin(mockAST, file); + const { + data: { files } + } = file; + const testObject = files[0]; + expect(Object.keys(testObject).length).toEqual(6); + expect(testObject).toHaveProperty('key'); + expect(testObject).toHaveProperty('ext'); + expect(testObject).toHaveProperty('name'); + expect(testObject).toHaveProperty('contents'); + expect(testObject).toHaveProperty('head'); + expect(testObject).toHaveProperty('tail'); + }); + + it('only adds strings to the `files` object type', () => { + expect.assertions(6); + plugin(mockAST, file); + const { + data: { files } + } = file; + const testObject = files[0]; + Object.keys(testObject) + .map(key => testObject[key]) + .forEach(value => expect(typeof value).toEqual('string')); + }); + + it('should have an output to match the snapshot', () => { + plugin(mockAST, file); + expect(file.data).toMatchSnapshot(); + }); +}); diff --git a/tools/challenge-md-parser/index.js b/tools/challenge-md-parser/index.js index 540d5cd64f..d9e20840ba 100644 --- a/tools/challenge-md-parser/index.js +++ b/tools/challenge-md-parser/index.js @@ -10,6 +10,7 @@ const raw = require('rehype-raw'); const frontmatterToData = require('./frontmatter-to-data'); const textToData = require('./text-to-data'); const testsToData = require('./tests-to-data'); +const challengeSeedToData = require('./challengeSeed-to-data'); const processor = unified() .use(markdown) @@ -19,6 +20,7 @@ const processor = unified() .use(remark2rehype, { allowDangerousHTML: true }) .use(raw) .use(textToData, ['description', 'instructions']) + .use(challengeSeedToData) // the plugins below are just to stop the processor from throwing // we need to write a compiler that can create graphql nodes .use(html); diff --git a/tools/challenge-md-parser/package-lock.json b/tools/challenge-md-parser/package-lock.json index e071f59b81..ccba42b032 100644 --- a/tools/challenge-md-parser/package-lock.json +++ b/tools/challenge-md-parser/package-lock.json @@ -821,6 +821,11 @@ } } }, + "bcp-47-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-1.0.0.tgz", + "integrity": "sha512-6rzWe2U70JHrYfBqBnhoeV7HykhmCQ6X2RrBcbpkyCwtNpkGadr3m3LF8ZLXpQ7qrsLCOm3eis2lsVemkLsvHg==" + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -831,6 +836,11 @@ "tweetnacl": "0.14.5" } }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1156,6 +1166,11 @@ } } }, + "css-selector-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-1.3.0.tgz", + "integrity": "sha1-XxrUPi2O77/cME/NOaUhZklD4+s=" + }, "cssom": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.4.tgz", @@ -1330,6 +1345,11 @@ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, + "direction": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/direction/-/direction-1.0.2.tgz", + "integrity": "sha512-hSKoz5FBn+zhP9vWKkVQaaxnRDg3/MoPdcg2au54HIUDR8MrP8Ah1jXSJwCXel6SV3Afh5DSzc8Uqv2r1UoQwQ==" + }, "domexception": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", @@ -2424,6 +2444,11 @@ "xtend": "4.0.1" } }, + "hast-util-has-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-1.0.1.tgz", + "integrity": "sha512-DUck5lp8ku3o8n9GIA1Nghdz8UQyis2/b/ro0O4z5HP/y82uzZL6CXehuQmY5re+rLgTP4MVF/YpYDj9YqD0wA==" + }, "hast-util-is-element": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-1.0.1.tgz", @@ -2456,6 +2481,27 @@ } } }, + "hast-util-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-2.1.0.tgz", + "integrity": "sha512-wPnaITEMGvTvzA9GUQhj7qyOVLceQfxol3Zj+2BrhuMQNt7/W1Za76ZDspNeRKAZmT4aFikNa2SwuJ7MaQC6LQ==", + "requires": { + "bcp-47-match": "1.0.0", + "comma-separated-tokens": "1.0.5", + "css-selector-parser": "1.3.0", + "direction": "1.0.2", + "hast-util-has-property": "1.0.1", + "hast-util-is-element": "1.0.1", + "hast-util-to-string": "1.0.1", + "hast-util-whitespace": "1.0.1", + "not": "0.1.0", + "nth-check": "1.0.1", + "property-information": "4.2.0", + "space-separated-tokens": "1.1.2", + "unist-util-visit": "1.4.0", + "zwitch": "1.0.3" + } + }, "hast-util-to-html": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-4.0.1.tgz", @@ -2485,6 +2531,11 @@ "zwitch": "1.0.3" } }, + "hast-util-to-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-1.0.1.tgz", + "integrity": "sha512-EC6awGe0ZMUNYmS2hMVaKZxvjVtQA4RhXjtgE20AxGG49MM7OUUfaHc6VcVYv2YwzNlrZQGe5teimCxW1Rk+fA==" + }, "hast-util-whitespace": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-1.0.1.tgz", @@ -3923,6 +3974,11 @@ "remove-trailing-separator": "1.1.0" } }, + "not": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/not/-/not-0.1.0.tgz", + "integrity": "sha1-yWkcF0bFXc++VMvYvU/wQbwrUZ0=" + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -3932,6 +3988,14 @@ "path-key": "2.0.1" } }, + "nth-check": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", + "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", + "requires": { + "boolbase": "1.0.0" + } + }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", diff --git a/tools/challenge-md-parser/package.json b/tools/challenge-md-parser/package.json index 5a746816d0..b2c4d13534 100644 --- a/tools/challenge-md-parser/package.json +++ b/tools/challenge-md-parser/package.json @@ -13,6 +13,7 @@ "jest": "^23.6.0" }, "dependencies": { + "hast-util-select": "^2.1.0", "hast-util-to-html": "^4.0.1", "js-yaml": "^3.12.0", "lodash": "^4.17.11", diff --git a/tools/challenge-md-parser/text-to-data.js b/tools/challenge-md-parser/text-to-data.js index b4576d0236..8ff7b8767a 100644 --- a/tools/challenge-md-parser/text-to-data.js +++ b/tools/challenge-md-parser/text-to-data.js @@ -1,12 +1,7 @@ const visit = require('unist-util-visit'); const toHTML = require('hast-util-to-html'); -const sectionFilter = ( - { type, tagName, properties: { id = '' } }, - sectionId -) => { - return type === 'element' && tagName === 'section' && id === sectionId; -}; +const { sectionFilter } = require('./utils') function textToData(sectionIds) { if (!sectionIds || !Array.isArray(sectionIds) || sectionIds.length <= 0) { diff --git a/tools/challenge-md-parser/utils/index.js b/tools/challenge-md-parser/utils/index.js new file mode 100644 index 0000000000..6c27bc3251 --- /dev/null +++ b/tools/challenge-md-parser/utils/index.js @@ -0,0 +1,6 @@ +exports.sectionFilter = ( + { type, tagName, properties: { id = '' } }, + sectionId +) => { + return type === 'element' && tagName === 'section' && id === sectionId; +};