mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-02-27 02:03:44 -05:00
feat(challenge-parser): add validateSections plugin (#61148)
This commit is contained in:
@@ -5,6 +5,7 @@ const { readSync } = require('to-vfile');
|
||||
const unified = require('unified');
|
||||
const addFillInTheBlank = require('./plugins/add-fill-in-the-blank');
|
||||
const addFrontmatter = require('./plugins/add-frontmatter');
|
||||
const validateSections = require('./plugins/validate-sections');
|
||||
const addSeed = require('./plugins/add-seed');
|
||||
const addSolution = require('./plugins/add-solution');
|
||||
const addHooks = require('./plugins/add-hooks');
|
||||
@@ -32,6 +33,8 @@ const processor = unified()
|
||||
.use(frontmatter, ['yaml'])
|
||||
// extract the content from that 'yaml' node
|
||||
.use(addFrontmatter)
|
||||
// validate all section markers before any plugin tries to extract sections
|
||||
.use(validateSections)
|
||||
// Any imports will be replaced (in the tree) with
|
||||
// the sub-tree of the target file. e.g.
|
||||
// ::import{component="Script" from="./file.path" }
|
||||
|
||||
126
tools/challenge-parser/parser/plugins/validate-sections.js
Normal file
126
tools/challenge-parser/parser/plugins/validate-sections.js
Normal file
@@ -0,0 +1,126 @@
|
||||
const { findAll } = require('./utils/find-all');
|
||||
const { isMarker } = require('./utils/get-section');
|
||||
|
||||
const VALID_MARKERS = [
|
||||
// Level 1
|
||||
'# --after-all--',
|
||||
'# --after-each--',
|
||||
'# --assignment--',
|
||||
'# --before-all--',
|
||||
'# --before-each--',
|
||||
'# --description--',
|
||||
'# --explanation--',
|
||||
'# --fillInTheBlank--',
|
||||
'# --hints--',
|
||||
'# --instructions--',
|
||||
'# --notes--',
|
||||
'# --questions--',
|
||||
'# --quizzes--',
|
||||
'# --scene--',
|
||||
'# --seed--',
|
||||
'# --solutions--',
|
||||
'# --transcript--',
|
||||
|
||||
// Level 2
|
||||
'## --answers--',
|
||||
'## --blanks--',
|
||||
'## --quiz--',
|
||||
'## --seed-contents--',
|
||||
'## --sentence--',
|
||||
'## --text--',
|
||||
'## --video-solution--',
|
||||
// TODO: Remove these two markers when https://github.com/freeCodeCamp/freeCodeCamp/issues/57107 is resolved
|
||||
'## --after-user-code--',
|
||||
'## --before-user-code--',
|
||||
|
||||
// Level 3
|
||||
'### --feedback--',
|
||||
'### --question--',
|
||||
|
||||
// Level 4
|
||||
'#### --answer--',
|
||||
'#### --distractors--',
|
||||
'#### --text--'
|
||||
];
|
||||
|
||||
// Special markers that should not be used as headings
|
||||
const NON_HEADING_MARKERS = ['--fcc-editable-region--'];
|
||||
|
||||
function validateSections() {
|
||||
function transformer(tree) {
|
||||
const allMarkers = findAll(tree, isMarker);
|
||||
|
||||
const invalidMarkerNames = [];
|
||||
const invalidHeadingLevels = [];
|
||||
const nonHeadingMarkersAsHeadings = [];
|
||||
|
||||
const errors = [];
|
||||
|
||||
for (const markerNode of allMarkers) {
|
||||
const markerValue = markerNode.children[0].value;
|
||||
const headingLevel = markerNode.depth;
|
||||
const fullMarker = '#'.repeat(headingLevel) + ' ' + markerValue;
|
||||
|
||||
if (NON_HEADING_MARKERS.includes(markerValue)) {
|
||||
nonHeadingMarkersAsHeadings.push(fullMarker);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!VALID_MARKERS.includes(fullMarker)) {
|
||||
const markerExistsAtAnyLevel = VALID_MARKERS.some(validMarker =>
|
||||
validMarker.endsWith(markerValue)
|
||||
);
|
||||
|
||||
if (markerExistsAtAnyLevel) {
|
||||
const validLevels = VALID_MARKERS.filter(validMarker =>
|
||||
validMarker.endsWith(markerValue)
|
||||
).map(validMarker => validMarker.split(' ')[0]); // Extract the # symbols
|
||||
|
||||
invalidHeadingLevels.push({
|
||||
fullMarker,
|
||||
markerValue,
|
||||
validLevels
|
||||
});
|
||||
} else {
|
||||
invalidMarkerNames.push(markerValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidMarkerNames.length > 0) {
|
||||
errors.push(
|
||||
`Invalid marker names: ${invalidMarkerNames.map(m => `"${m}"`).join(', ')}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (nonHeadingMarkersAsHeadings.length > 0) {
|
||||
errors.push(
|
||||
`Non-heading markers should not be used as headings: ${nonHeadingMarkersAsHeadings.map(m => `"${m}"`).join(', ')}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (invalidHeadingLevels.length > 0) {
|
||||
const levelErrors = invalidHeadingLevels.map(
|
||||
({ fullMarker, markerValue, validLevels }) => {
|
||||
const validText =
|
||||
validLevels.length === 1
|
||||
? `${validLevels[0]} ${markerValue}`
|
||||
: validLevels
|
||||
.map(level => `${level} ${markerValue}`)
|
||||
.join(' or ');
|
||||
return `"${fullMarker}" should be "${validText}"`;
|
||||
}
|
||||
);
|
||||
|
||||
errors.push(`Invalid heading levels: ${levelErrors.join(', ')}.`);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
return transformer;
|
||||
}
|
||||
|
||||
module.exports = validateSections;
|
||||
248
tools/challenge-parser/parser/plugins/validate-sections.test.js
Normal file
248
tools/challenge-parser/parser/plugins/validate-sections.test.js
Normal file
@@ -0,0 +1,248 @@
|
||||
const unified = require('unified');
|
||||
const remark = require('remark-parse');
|
||||
const frontmatter = require('remark-frontmatter');
|
||||
const addFrontmatter = require('./add-frontmatter');
|
||||
const validateSections = require('./validate-sections');
|
||||
|
||||
const processor = unified()
|
||||
.use(remark)
|
||||
.use(frontmatter, ['yaml'])
|
||||
.use(addFrontmatter)
|
||||
.use(validateSections);
|
||||
|
||||
describe('validate-sections plugin', () => {
|
||||
it('should pass when all section markers are valid', () => {
|
||||
const file = `---
|
||||
id: test
|
||||
title: Test
|
||||
---
|
||||
|
||||
# --after-all--
|
||||
After all hook.
|
||||
|
||||
# --after-each--
|
||||
After each hook.
|
||||
|
||||
# --assignment--
|
||||
Assignment.
|
||||
|
||||
# --before-all--
|
||||
Before all hook.
|
||||
|
||||
# --before-each--
|
||||
Before each hook.
|
||||
|
||||
# --description--
|
||||
Text content.
|
||||
|
||||
# --explanation--
|
||||
Explanation.
|
||||
|
||||
# --fillInTheBlank--
|
||||
Fill in blank.
|
||||
|
||||
# --hints--
|
||||
Hints.
|
||||
|
||||
# --instructions--
|
||||
More text.
|
||||
|
||||
# --notes--
|
||||
Notes.
|
||||
|
||||
# --questions--
|
||||
Video questions.
|
||||
|
||||
# --quizzes--
|
||||
Quiz section.
|
||||
|
||||
# --scene--
|
||||
Scene content.
|
||||
|
||||
# --seed--
|
||||
Seed section.
|
||||
|
||||
# --solutions--
|
||||
Solutions.
|
||||
|
||||
# --transcript--
|
||||
Transcript.
|
||||
|
||||
## --answers--
|
||||
Answers.
|
||||
|
||||
## --blanks--
|
||||
Blanks.
|
||||
|
||||
## --quiz--
|
||||
Individual quiz.
|
||||
|
||||
## --seed-contents--
|
||||
Contents here.
|
||||
|
||||
## --sentence--
|
||||
Sentence.
|
||||
|
||||
## --text--
|
||||
Question text.
|
||||
|
||||
## --video-solution--
|
||||
Video solution.
|
||||
|
||||
## --after-user-code--
|
||||
After code.
|
||||
|
||||
## --before-user-code--
|
||||
Before code.
|
||||
|
||||
### --feedback--
|
||||
Feedback.
|
||||
|
||||
### --question--
|
||||
Quiz question.
|
||||
|
||||
#### --answer--
|
||||
Correct answer.
|
||||
|
||||
#### --distractors--
|
||||
Distractors.
|
||||
|
||||
#### --text--
|
||||
Question text.
|
||||
|
||||
--fcc-editable-region--
|
||||
Editable region.
|
||||
`;
|
||||
|
||||
expect(() => {
|
||||
processor.runSync(processor.parse(file));
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error for invalid marker names', () => {
|
||||
const file = `---
|
||||
id: test
|
||||
title: Test
|
||||
---
|
||||
|
||||
# --descriptio--
|
||||
Typo in marker name.
|
||||
|
||||
# --instructionss--
|
||||
Another typo.
|
||||
|
||||
# -- instructions--
|
||||
Another typo.
|
||||
|
||||
# --feedback---
|
||||
Another typo.
|
||||
|
||||
# --invalid-marker--
|
||||
Completely invalid marker.
|
||||
`;
|
||||
|
||||
expect(() => {
|
||||
processor.runSync(processor.parse(file));
|
||||
}).toThrow(
|
||||
'Invalid marker names: "--descriptio--", "--instructionss--", "-- instructions--", "--feedback---", "--invalid-marker--".'
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate case-sensitive markers', () => {
|
||||
const file = `---
|
||||
id: test
|
||||
title: Test
|
||||
---
|
||||
|
||||
# --INSTRUCTIONS--
|
||||
Wrong case.
|
||||
|
||||
# --Instructions--
|
||||
Also wrong case.
|
||||
`;
|
||||
|
||||
expect(() => {
|
||||
processor.runSync(processor.parse(file));
|
||||
}).toThrow('Invalid marker names: "--INSTRUCTIONS--", "--Instructions--".');
|
||||
});
|
||||
|
||||
it('should throw error for correct marker at wrong heading level', () => {
|
||||
const file = `---
|
||||
id: test
|
||||
title: Test
|
||||
---
|
||||
|
||||
## --instructions--
|
||||
Instructions should be at level 1, not 2.
|
||||
|
||||
### --seed-contents--
|
||||
Seed contents should be at level 2, not 3.
|
||||
`;
|
||||
|
||||
expect(() => {
|
||||
processor.runSync(processor.parse(file));
|
||||
}).toThrow(
|
||||
'Invalid heading levels: "## --instructions--" should be "# --instructions--", "### --seed-contents--" should be "## --seed-contents--".'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw combined errors for invalid markers and wrong levels', () => {
|
||||
const file = `---
|
||||
id: test
|
||||
title: Test
|
||||
---
|
||||
|
||||
## --instructions--
|
||||
Wrong level.
|
||||
|
||||
# --invalid-marker--
|
||||
Invalid marker.
|
||||
|
||||
### --seed-contents--
|
||||
Wrong level.
|
||||
`;
|
||||
|
||||
expect(() => {
|
||||
processor.runSync(processor.parse(file));
|
||||
}).toThrow(
|
||||
'Invalid marker names: "--invalid-marker--".\nInvalid heading levels: "## --instructions--" should be "# --instructions--", "### --seed-contents--" should be "## --seed-contents--".'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for fcc-editable-region when used as headings', () => {
|
||||
const file = `---
|
||||
id: test
|
||||
title: Test
|
||||
---
|
||||
|
||||
# --fcc-editable-region--
|
||||
This should not be a heading.
|
||||
|
||||
## --fcc-editable-region--
|
||||
This should also not be a heading.
|
||||
`;
|
||||
|
||||
expect(() => {
|
||||
processor.runSync(processor.parse(file));
|
||||
}).toThrow(
|
||||
'Non-heading markers should not be used as headings: "# --fcc-editable-region--", "## --fcc-editable-region--".'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for markers valid at multiple levels but used at an invalid level', () => {
|
||||
const file = `---
|
||||
id: test
|
||||
title: Test
|
||||
---
|
||||
|
||||
### --text--
|
||||
This marker is valid at level 2 or level 4, but not at level 3.
|
||||
`;
|
||||
|
||||
expect(() => {
|
||||
processor.runSync(processor.parse(file));
|
||||
}).toThrow(
|
||||
'Invalid heading levels: "### --text--" should be "## --text-- or #### --text--".'
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user