From 8478e021bf1a2cac28549963a34564140d95f40e Mon Sep 17 00:00:00 2001 From: Randell Dawson <5313213+RandellDawson@users.noreply.github.com> Date: Wed, 1 Jul 2020 23:14:46 -0700 Subject: [PATCH] feat (learn): Remove editable regions from seed code before displaying user's code (#39153) * feat: pull editable region from markdown * test: update seed tests to reflect new schema * feat(curriculum): validate multi-file solutions * test: add editableRegionBoundaries to schema Co-authored-by: Oliver Eyton-Williams --- curriculum/schema/challengeSchema.js | 31 +++++------ .../challengeSeed-to-data.test.js.snap | 1 + .../challengeSeed-to-data.js | 51 +++++++++++++++++++ .../challengeSeed-to-data.test.js | 25 ++++----- 4 files changed, 75 insertions(+), 33 deletions(-) diff --git a/curriculum/schema/challengeSchema.js b/curriculum/schema/challengeSchema.js index 5860353163..78d75b7d27 100644 --- a/curriculum/schema/challengeSchema.js +++ b/curriculum/schema/challengeSchema.js @@ -3,6 +3,16 @@ Joi.objectId = require('joi-objectid')(Joi); const { challengeTypes } = require('../../client/utils/challengeTypes'); +const fileJoi = Joi.object().keys({ + key: Joi.string(), + ext: Joi.string(), + name: Joi.string(), + head: [Joi.array().items(Joi.string().allow('')), Joi.string().allow('')], + tail: [Joi.array().items(Joi.string().allow('')), Joi.string().allow('')], + contents: [Joi.array().items(Joi.string().allow('')), Joi.string().allow('')], + editableRegionBoundaries: [Joi.array().items(Joi.string().allow(''))] +}); + function getSchemaForLang(lang) { let schema = Joi.object().keys({ block: Joi.string(), @@ -20,25 +30,7 @@ function getSchemaForLang(lang) { otherwise: Joi.string().required() }), fileName: Joi.string(), - files: Joi.array().items( - Joi.object().keys({ - key: Joi.string(), - ext: Joi.string(), - name: Joi.string(), - head: [ - Joi.array().items(Joi.string().allow('')), - Joi.string().allow('') - ], - tail: [ - Joi.array().items(Joi.string().allow('')), - Joi.string().allow('') - ], - contents: [ - Joi.array().items(Joi.string().allow('')), - Joi.string().allow('') - ] - }) - ), + files: Joi.array().items(fileJoi), guideUrl: Joi.string().uri({ scheme: 'https' }), videoUrl: Joi.string().allow(''), forumTopicId: Joi.number(), @@ -72,6 +64,7 @@ function getSchemaForLang(lang) { }) ), solutions: Joi.array().items(Joi.string().optional()), + solutionFiles: Joi.array().items(fileJoi), superBlock: Joi.string(), superOrder: Joi.number(), suborder: Joi.number(), 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 index 4076256e78..5c1226bca2 100644 --- a/tools/challenge-md-parser/__snapshots__/challengeSeed-to-data.test.js.snap +++ b/tools/challenge-md-parser/__snapshots__/challengeSeed-to-data.test.js.snap @@ -10,6 +10,7 @@ Object { testFunction('hello'); ", + "editableRegionBoundaries": Array [], "ext": "js", "head": "console.log('before the test'); ", diff --git a/tools/challenge-md-parser/challengeSeed-to-data.js b/tools/challenge-md-parser/challengeSeed-to-data.js index fae6b912bd..501e55308f 100644 --- a/tools/challenge-md-parser/challengeSeed-to-data.js +++ b/tools/challenge-md-parser/challengeSeed-to-data.js @@ -7,6 +7,8 @@ const seedRE = /(.+)-seed$/; const headRE = /(.+)-setup$/; const tailRE = /(.+)-teardown$/; +const editableRegionMarker = '--fcc-editable-region--'; + function defaultFile(lang) { return { key: `index${lang}`, @@ -38,6 +40,32 @@ function createCodeGetter(key, regEx, seeds) { }; } +// TODO: any reason to worry about CRLF? + +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'); +} + function createPlugin() { return function transformer(tree, file) { function visitor(node) { @@ -63,6 +91,29 @@ function createPlugin() { ...file.data, files: Object.keys(seeds).map(lang => seeds[lang]) }; + + // TODO: make this readable. + + if (file.data.files) { + file.data.files.forEach(fileData => { + 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 = []; + } + }); + } + + // TODO: TESTS! } } visit(tree, 'element', visitor); diff --git a/tools/challenge-md-parser/challengeSeed-to-data.test.js b/tools/challenge-md-parser/challengeSeed-to-data.test.js index 449256965b..489886e04b 100644 --- a/tools/challenge-md-parser/challengeSeed-to-data.test.js +++ b/tools/challenge-md-parser/challengeSeed-to-data.test.js @@ -1,6 +1,7 @@ /* global describe it expect beforeEach */ const mockAST = require('./fixtures/challenge-html-ast.json'); const challengeSeedToData = require('./challengeSeed-to-data'); +const isArray = require('lodash/isArray'); describe('challengeSeed-to-data plugin', () => { const plugin = challengeSeedToData(); @@ -25,31 +26,27 @@ describe('challengeSeed-to-data plugin', () => { }); it('adds test objects to the files array following a schema', () => { - expect.assertions(7); + expect.assertions(15); plugin(mockAST, file); const { data: { files } } = file; const testObject = files[0]; - expect(Object.keys(testObject).length).toEqual(6); + 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'); - }); - - 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')); + expect(typeof testObject['tail']).toBe('string'); + expect(testObject).toHaveProperty('editableRegionBoundaries'); + expect(isArray(testObject['editableRegionBoundaries'])).toBe(true); }); it('should have an output to match the snapshot', () => {