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 <ojeytonwilliams@gmail.com>
This commit is contained in:
		
				
					committed by
					
						 Mrugesh Mohapatra
						Mrugesh Mohapatra
					
				
			
			
				
	
			
			
			
						parent
						
							fd7a8c0d5e
						
					
				
				
					commit
					8478e021bf
				
			| @@ -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(), | ||||
|   | ||||
| @@ -10,6 +10,7 @@ Object { | ||||
|  | ||||
| testFunction('hello'); | ||||
| ", | ||||
|       "editableRegionBoundaries": Array [], | ||||
|       "ext": "js", | ||||
|       "head": "console.log('before the test'); | ||||
| ", | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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', () => { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user