feat(curriculum): restore seed + solution to Chinese (#40683)
* feat(tools): add seed/solution restore script * chore(curriculum): remove empty sections' markers * chore(curriculum): add seed + solution to Chinese * chore: remove old formatter * fix: update getChallenges parse translated challenges separately, without reference to the source * chore(curriculum): add dashedName to English * chore(curriculum): add dashedName to Chinese * refactor: remove unused challenge property 'name' * fix: relax dashedName requirement * fix: stray tag Remove stray `pre` tag from challenge file. Signed-off-by: nhcarrigan <nhcarrigan@gmail.com> Co-authored-by: nhcarrigan <nhcarrigan@gmail.com>
This commit is contained in:
committed by
GitHub
parent
0095583028
commit
ee1e8abd87
202
tools/formatter/add-seed-and-solution/create-challenge-string.js
Normal file
202
tools/formatter/add-seed-and-solution/create-challenge-string.js
Normal file
@@ -0,0 +1,202 @@
|
||||
const { isEmpty, pick } = require('lodash');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const frontmatterProperties = [
|
||||
'id',
|
||||
'title',
|
||||
'challengeType',
|
||||
'videoId',
|
||||
'videoUrl',
|
||||
'forumTopicId',
|
||||
'isPrivate',
|
||||
'required',
|
||||
'helpCategory',
|
||||
'dashedName'
|
||||
];
|
||||
|
||||
const otherProperties = [
|
||||
'description',
|
||||
'instructions',
|
||||
'tests',
|
||||
'solutions',
|
||||
'files',
|
||||
'question'
|
||||
];
|
||||
|
||||
function createFrontmatter(data) {
|
||||
Object.keys(data).forEach(key => {
|
||||
if (!frontmatterProperties.includes(key) && !otherProperties.includes(key))
|
||||
throw Error(`Unknown property '${key}'`);
|
||||
});
|
||||
|
||||
// TODO: sort the keys? It doesn't matter from a machine perspective, but
|
||||
// it does from human-readability one. We could get lucky and have the order
|
||||
// be preserved accidentally.
|
||||
const frontData = pick(data, frontmatterProperties);
|
||||
const frontYAML = yaml.dump(frontData);
|
||||
|
||||
return `---
|
||||
${frontYAML}---
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
// NOTE: trimEnd is used since trailing whitespace is rarely used (it can create
|
||||
// a <br>, but that's uncommon and hard to read)
|
||||
function createHints({ tests }) {
|
||||
if (!tests) return '';
|
||||
const strTests = tests
|
||||
.map(
|
||||
({ text, testString }) => `${text.trimEnd()}
|
||||
|
||||
${testString.trimEnd()}
|
||||
`
|
||||
)
|
||||
.join('\n');
|
||||
return createSection('hints', strTests);
|
||||
}
|
||||
|
||||
function createQuestion({ question }) {
|
||||
if (!question) return '';
|
||||
const { text, answers, solution } = question;
|
||||
const formattedAnswers = answers.map(
|
||||
answer => `${answer.trimEnd()}
|
||||
`
|
||||
).join(`
|
||||
---
|
||||
|
||||
`);
|
||||
const formattedQuestion =
|
||||
createSection('text', text, { depth: 2 }) +
|
||||
createSection('answers', formattedAnswers, { depth: 2 }) +
|
||||
createSection('video-solution', solution, { depth: 2 });
|
||||
return createSection('question', formattedQuestion);
|
||||
}
|
||||
|
||||
function createInstructions({ instructions }) {
|
||||
return createSection('instructions', instructions);
|
||||
}
|
||||
|
||||
function createDescription({ description }) {
|
||||
return createSection('description', description);
|
||||
}
|
||||
|
||||
function createSeed({ files }) {
|
||||
if (!files) return '';
|
||||
const supportedLanguages = ['html', 'css', 'js', 'jsx'];
|
||||
const supportedIndexes = supportedLanguages.map(lang => 'index' + lang);
|
||||
Object.values(files).forEach(({ ext }) => {
|
||||
if (!supportedLanguages.includes(ext)) throw `Unsupported language: ${ext}`;
|
||||
});
|
||||
Object.keys(files).forEach(index => {
|
||||
if (!supportedIndexes.includes(index)) throw `Unsupported index: ${index}`;
|
||||
});
|
||||
|
||||
const head = Object.values(files)
|
||||
.filter(({ head }) => !isEmpty(head))
|
||||
.map(({ ext, head }) => fenceCode(ext, head))
|
||||
.join('\n');
|
||||
|
||||
const tail = Object.values(files)
|
||||
.filter(({ tail }) => !isEmpty(tail))
|
||||
.map(({ ext, tail }) => fenceCode(ext, tail))
|
||||
.join('\n');
|
||||
|
||||
const contents = Object.values(files)
|
||||
.map(({ ext, contents, editableRegionBoundaries }) =>
|
||||
fenceCode(ext, insertMarkers(contents, editableRegionBoundaries))
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
return (
|
||||
`# --seed--
|
||||
|
||||
` +
|
||||
createSection('before-user-code', head, { depth: 2 }) +
|
||||
createSection('after-user-code', tail, { depth: 2 }) +
|
||||
createSection('seed-contents', contents, { depth: 2 })
|
||||
);
|
||||
}
|
||||
|
||||
function createSolutions({ solutions }) {
|
||||
if (isEmptySolutions(solutions)) return '';
|
||||
const solutionsStr = solutions.map(soln => solutionToText(soln)).join(`
|
||||
---
|
||||
|
||||
`);
|
||||
return `# --solutions--
|
||||
|
||||
${solutionsStr}`;
|
||||
}
|
||||
|
||||
function isEmptySolutions(solutions) {
|
||||
if (!solutions || !solutions.length) return true;
|
||||
return isEmpty(solutions[0]);
|
||||
}
|
||||
|
||||
function solutionToText(solution) {
|
||||
const supportedLanguages = ['html', 'css', 'js', 'jsx', 'py'];
|
||||
const supportedIndexes = supportedLanguages.map(lang => 'index' + lang);
|
||||
Object.values(solution).forEach(({ ext }) => {
|
||||
if (!supportedLanguages.includes(ext)) throw `Unsupported language: ${ext}`;
|
||||
});
|
||||
Object.keys(solution).forEach(index => {
|
||||
if (!supportedIndexes.includes(index)) throw `Unsupported index: ${index}`;
|
||||
});
|
||||
|
||||
return Object.values(solution)
|
||||
.map(({ ext, contents }) => fenceCode(ext, contents))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function createSection(heading, contents, options) {
|
||||
const { depth = 1 } = options || {};
|
||||
return contents && contents.toString().trim()
|
||||
? `${''.padEnd(depth, '#')} --${heading}--
|
||||
|
||||
${contents.toString().trimEnd()}
|
||||
|
||||
`
|
||||
: '';
|
||||
}
|
||||
|
||||
// Even if there is no code, we should fence it in case the extension is used
|
||||
function fenceCode(ext, code) {
|
||||
if (code) {
|
||||
return `${'```' + ext}
|
||||
${code}
|
||||
${'```'}
|
||||
`;
|
||||
} else {
|
||||
return `${'```' + ext}
|
||||
${'```'}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function insertMarkers(code, markers) {
|
||||
const lines = code.split('\n');
|
||||
return markers
|
||||
.reduce((acc, idx) => {
|
||||
return insert(acc, '--fcc-editable-region--', idx);
|
||||
}, lines)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function insert(xs, x, idx) {
|
||||
return [...xs.slice(0, idx), x, ...xs.slice(idx)];
|
||||
}
|
||||
|
||||
function challengeToString(data) {
|
||||
const chalString =
|
||||
createFrontmatter(data) +
|
||||
createDescription(data) +
|
||||
createInstructions(data) +
|
||||
createQuestion(data) +
|
||||
createHints(data) +
|
||||
createSeed(data) +
|
||||
createSolutions(data);
|
||||
return chalString;
|
||||
}
|
||||
|
||||
exports.challengeToString = challengeToString;
|
Reference in New Issue
Block a user