2017-01-18 23:16:23 +00:00
|
|
|
const path = require('path');
|
2020-06-08 13:01:48 +00:00
|
|
|
const { findIndex, reduce, toString } = require('lodash');
|
2018-10-04 13:45:22 +00:00
|
|
|
const readDirP = require('readdirp-walk');
|
2020-07-27 12:41:53 +00:00
|
|
|
const { parseMarkdown } = require('../tools/challenge-md-parser');
|
2020-04-23 17:01:15 +00:00
|
|
|
const fs = require('fs');
|
2020-08-28 15:10:37 +00:00
|
|
|
const util = require('util');
|
2020-02-24 14:05:18 +00:00
|
|
|
/* eslint-disable max-len */
|
|
|
|
const {
|
|
|
|
mergeChallenges,
|
|
|
|
translateCommentsInChallenge
|
|
|
|
} = require('../tools/challenge-md-parser/translation-parser/translation-parser');
|
|
|
|
/* eslint-enable max-len*/
|
2018-01-19 19:03:17 +00:00
|
|
|
|
2020-02-25 09:20:14 +00:00
|
|
|
const { isAuditedCert } = require('../utils/is-audited');
|
2020-06-08 13:01:48 +00:00
|
|
|
const { dasherize, nameify } = require('../utils/slugs');
|
|
|
|
const { createPoly } = require('../utils/polyvinyl');
|
|
|
|
const { blockNameify } = require('../utils/block-nameify');
|
2020-08-28 10:21:44 +00:00
|
|
|
const { supportedLangs } = require('./utils');
|
2020-10-30 19:10:34 +00:00
|
|
|
const { helpCategoryMap } = require('../client/utils/challengeTypes');
|
2018-10-28 06:18:13 +00:00
|
|
|
|
2020-08-28 15:10:37 +00:00
|
|
|
const access = util.promisify(fs.access);
|
|
|
|
|
2018-10-09 19:26:37 +00:00
|
|
|
const challengesDir = path.resolve(__dirname, './challenges');
|
2018-11-16 18:22:52 +00:00
|
|
|
const metaDir = path.resolve(challengesDir, '_meta');
|
|
|
|
exports.challengesDir = challengesDir;
|
|
|
|
exports.metaDir = metaDir;
|
2018-10-09 19:26:37 +00:00
|
|
|
|
2020-09-28 13:16:30 +00:00
|
|
|
const COMMENT_TRANSLATIONS = createCommentMap(
|
|
|
|
path.resolve(__dirname, './dictionaries')
|
|
|
|
);
|
|
|
|
|
2020-09-23 14:38:20 +00:00
|
|
|
function getTranslatableComments(dictionariesDir) {
|
|
|
|
const { COMMENTS_TO_TRANSLATE } = require(path.resolve(
|
|
|
|
dictionariesDir,
|
|
|
|
'english',
|
|
|
|
'comments'
|
|
|
|
));
|
|
|
|
return COMMENTS_TO_TRANSLATE.map(({ text }) => text);
|
|
|
|
}
|
|
|
|
|
|
|
|
exports.getTranslatableComments = getTranslatableComments;
|
|
|
|
|
2020-09-28 13:16:30 +00:00
|
|
|
function createCommentMap(dictionariesDir) {
|
|
|
|
// get all the languages for which there are dictionaries.
|
|
|
|
const languages = fs
|
|
|
|
.readdirSync(dictionariesDir)
|
|
|
|
.filter(x => x !== 'english');
|
|
|
|
|
|
|
|
// get all their dictionaries
|
|
|
|
const dictionaries = languages.reduce(
|
|
|
|
(acc, lang) => ({
|
|
|
|
...acc,
|
|
|
|
[lang]: require(path.resolve(dictionariesDir, lang, 'comments'))
|
|
|
|
}),
|
|
|
|
{}
|
|
|
|
);
|
|
|
|
|
|
|
|
// get the english dicts
|
|
|
|
const {
|
|
|
|
COMMENTS_TO_TRANSLATE,
|
|
|
|
COMMENTS_TO_NOT_TRANSLATE
|
|
|
|
} = require(path.resolve(dictionariesDir, 'english', 'comments'));
|
|
|
|
|
|
|
|
// map from english comment text to translations
|
|
|
|
const translatedCommentMap = COMMENTS_TO_TRANSLATE.reduce(
|
|
|
|
(acc, { id, text }) => {
|
|
|
|
return {
|
|
|
|
...acc,
|
|
|
|
[text]: getTranslationEntry(dictionaries, { engId: id, text })
|
|
|
|
};
|
|
|
|
},
|
|
|
|
{}
|
|
|
|
);
|
|
|
|
|
|
|
|
// map from english comment text to itself
|
|
|
|
const untranslatableCommentMap = COMMENTS_TO_NOT_TRANSLATE.reduce(
|
|
|
|
(acc, { text }) => {
|
|
|
|
const englishEntry = languages.reduce(
|
|
|
|
(acc, lang) => ({
|
|
|
|
...acc,
|
|
|
|
[lang]: text
|
|
|
|
}),
|
|
|
|
{}
|
|
|
|
);
|
|
|
|
return {
|
|
|
|
...acc,
|
|
|
|
[text]: englishEntry
|
|
|
|
};
|
|
|
|
},
|
|
|
|
{}
|
|
|
|
);
|
|
|
|
|
|
|
|
return { ...translatedCommentMap, ...untranslatableCommentMap };
|
|
|
|
}
|
|
|
|
|
|
|
|
exports.createCommentMap = createCommentMap;
|
|
|
|
|
|
|
|
function getTranslationEntry(dicts, { engId, text }) {
|
|
|
|
return Object.keys(dicts).reduce((acc, lang) => {
|
|
|
|
const entry = dicts[lang].find(({ id }) => engId === id);
|
|
|
|
if (entry) {
|
|
|
|
return { ...acc, [lang]: entry.text };
|
|
|
|
} else {
|
|
|
|
throw Error(`Missing translation for comment
|
|
|
|
'${text}'
|
|
|
|
with id of ${engId}`);
|
|
|
|
}
|
|
|
|
}, {});
|
|
|
|
}
|
|
|
|
|
2018-11-18 18:04:04 +00:00
|
|
|
function getChallengesDirForLang(lang) {
|
|
|
|
return path.resolve(challengesDir, `./${lang}`);
|
|
|
|
}
|
|
|
|
|
2020-04-23 17:01:15 +00:00
|
|
|
function getMetaForBlock(block) {
|
|
|
|
return JSON.parse(
|
|
|
|
fs.readFileSync(path.resolve(metaDir, `./${block}/meta.json`), 'utf8')
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-11-18 18:04:04 +00:00
|
|
|
exports.getChallengesDirForLang = getChallengesDirForLang;
|
2020-04-23 17:01:15 +00:00
|
|
|
exports.getMetaForBlock = getMetaForBlock;
|
2018-11-18 18:04:04 +00:00
|
|
|
|
2018-10-04 13:45:22 +00:00
|
|
|
exports.getChallengesForLang = function getChallengesForLang(lang) {
|
|
|
|
let curriculum = {};
|
2019-03-15 21:12:56 +00:00
|
|
|
return new Promise(resolve => {
|
|
|
|
let running = 1;
|
|
|
|
function done() {
|
|
|
|
if (--running === 0) {
|
|
|
|
resolve(curriculum);
|
|
|
|
}
|
|
|
|
}
|
2018-11-18 18:04:04 +00:00
|
|
|
readDirP({ root: getChallengesDirForLang(lang) })
|
2019-03-15 21:12:56 +00:00
|
|
|
.on('data', file => {
|
|
|
|
running++;
|
2020-08-28 15:10:37 +00:00
|
|
|
buildCurriculum(file, curriculum, lang).then(done);
|
2019-03-15 21:12:56 +00:00
|
|
|
})
|
|
|
|
.on('end', done);
|
|
|
|
});
|
2018-10-04 13:45:22 +00:00
|
|
|
};
|
|
|
|
|
2020-08-28 15:10:37 +00:00
|
|
|
async function buildCurriculum(file, curriculum, lang) {
|
|
|
|
const { name, depth, path: filePath, stat } = file;
|
|
|
|
const createChallenge = createChallengeCreator(challengesDir, lang);
|
2018-10-04 13:45:22 +00:00
|
|
|
if (depth === 1 && stat.isDirectory()) {
|
|
|
|
// extract the superBlock info
|
|
|
|
const { order, name: superBlock } = superBlockInfo(name);
|
|
|
|
curriculum[superBlock] = { superBlock, order, blocks: {} };
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (depth === 2 && stat.isDirectory()) {
|
2018-10-09 19:26:37 +00:00
|
|
|
const blockName = getBlockNameFromPath(filePath);
|
|
|
|
const metaPath = path.resolve(
|
|
|
|
__dirname,
|
|
|
|
`./challenges/_meta/${blockName}/meta.json`
|
|
|
|
);
|
|
|
|
const blockMeta = require(metaPath);
|
2020-09-03 22:07:40 +00:00
|
|
|
const { isUpcomingChange } = blockMeta;
|
|
|
|
if (typeof isUpcomingChange !== 'boolean') {
|
|
|
|
throw Error(
|
|
|
|
`meta file at ${metaPath} is missing 'isUpcomingChange', it must be 'true' or 'false'`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!isUpcomingChange || process.env.SHOW_UPCOMING_CHANGES === 'true') {
|
|
|
|
// add the block to the superBlock
|
|
|
|
const { name: superBlock } = superBlockInfoFromPath(filePath);
|
|
|
|
const blockInfo = { meta: blockMeta, challenges: [] };
|
|
|
|
curriculum[superBlock].blocks[name] = blockInfo;
|
|
|
|
}
|
2018-10-04 13:45:22 +00:00
|
|
|
return;
|
|
|
|
}
|
2018-10-10 20:20:40 +00:00
|
|
|
if (name === 'meta.json' || name === '.DS_Store') {
|
2018-10-04 13:45:22 +00:00
|
|
|
return;
|
|
|
|
}
|
2018-11-16 18:22:52 +00:00
|
|
|
|
2018-10-09 19:26:37 +00:00
|
|
|
const block = getBlockNameFromPath(filePath);
|
|
|
|
const { name: superBlock } = superBlockInfoFromPath(filePath);
|
2018-10-10 20:20:40 +00:00
|
|
|
let challengeBlock;
|
2020-09-03 22:07:40 +00:00
|
|
|
|
|
|
|
// TODO: this try block and process exit can all go once errors terminate the
|
|
|
|
// tests correctly.
|
2018-10-10 20:20:40 +00:00
|
|
|
try {
|
|
|
|
challengeBlock = curriculum[superBlock].blocks[block];
|
2020-09-03 22:07:40 +00:00
|
|
|
if (!challengeBlock) {
|
|
|
|
// this should only happen when a isUpcomingChange block is skipped
|
|
|
|
return;
|
|
|
|
}
|
2018-10-10 20:20:40 +00:00
|
|
|
} catch (e) {
|
2020-09-03 22:07:40 +00:00
|
|
|
console.log(`failed to create superBlock ${superBlock}`);
|
2018-10-25 09:54:57 +00:00
|
|
|
// eslint-disable-next-line no-process-exit
|
2020-09-03 22:07:40 +00:00
|
|
|
process.exit(1);
|
2018-10-10 20:20:40 +00:00
|
|
|
}
|
2018-10-04 13:45:22 +00:00
|
|
|
const { meta } = challengeBlock;
|
2018-11-16 18:22:52 +00:00
|
|
|
|
2020-08-28 15:10:37 +00:00
|
|
|
const challenge = await createChallenge(filePath, meta);
|
2018-11-16 18:22:52 +00:00
|
|
|
|
|
|
|
challengeBlock.challenges = [...challengeBlock.challenges, challenge];
|
|
|
|
}
|
|
|
|
|
2020-08-28 15:10:37 +00:00
|
|
|
async function parseTranslation(engPath, transPath, dict, lang) {
|
2020-02-24 14:05:18 +00:00
|
|
|
const engChal = await parseMarkdown(engPath);
|
|
|
|
const translatedChal = await parseMarkdown(transPath);
|
|
|
|
|
2020-09-28 13:13:18 +00:00
|
|
|
// challengeType 11 is for video challenges, which have no seeds, so we skip
|
|
|
|
// them.
|
|
|
|
const engWithTranslatedComments =
|
|
|
|
engChal.challengeType !== 11
|
|
|
|
? translateCommentsInChallenge(engChal, lang, dict)
|
|
|
|
: engChal;
|
2020-02-24 14:05:18 +00:00
|
|
|
return mergeChallenges(engWithTranslatedComments, translatedChal);
|
|
|
|
}
|
|
|
|
|
2020-08-28 15:10:37 +00:00
|
|
|
function createChallengeCreator(basePath, lang) {
|
|
|
|
const hasEnglishSource = hasEnglishSourceCreator(basePath);
|
|
|
|
return async function createChallenge(filePath, maybeMeta) {
|
2020-08-31 12:19:19 +00:00
|
|
|
function getFullPath(pathLang) {
|
|
|
|
return path.resolve(__dirname, basePath, pathLang, filePath);
|
|
|
|
}
|
2020-08-28 15:10:37 +00:00
|
|
|
let meta;
|
|
|
|
if (maybeMeta) {
|
|
|
|
meta = maybeMeta;
|
|
|
|
} else {
|
|
|
|
const metaPath = path.resolve(
|
|
|
|
metaDir,
|
|
|
|
`./${getBlockNameFromPath(filePath)}/meta.json`
|
|
|
|
);
|
|
|
|
meta = require(metaPath);
|
|
|
|
}
|
|
|
|
const { name: superBlock } = superBlockInfoFromPath(filePath);
|
|
|
|
if (!supportedLangs.includes(lang))
|
|
|
|
throw Error(`${lang} is not a accepted language.
|
|
|
|
Trying to parse ${filePath}`);
|
|
|
|
if (lang !== 'english' && !(await hasEnglishSource(filePath)))
|
|
|
|
throw Error(`Missing English challenge for
|
|
|
|
${filePath}
|
|
|
|
It should be in
|
2020-08-31 12:19:19 +00:00
|
|
|
${getFullPath('english')}
|
2020-08-28 15:10:37 +00:00
|
|
|
`);
|
|
|
|
// assumes superblock names are unique
|
|
|
|
// while the auditing is ongoing, we default to English for un-audited certs
|
|
|
|
// once that's complete, we can revert to using isEnglishChallenge(fullPath)
|
|
|
|
const useEnglish = lang === 'english' || !isAuditedCert(lang, superBlock);
|
|
|
|
const challenge = await (useEnglish
|
2020-08-31 12:19:19 +00:00
|
|
|
? parseMarkdown(getFullPath('english'))
|
2020-08-28 15:10:37 +00:00
|
|
|
: parseTranslation(
|
2020-08-31 12:19:19 +00:00
|
|
|
getFullPath('english'),
|
|
|
|
getFullPath(lang),
|
2020-08-28 15:10:37 +00:00
|
|
|
COMMENT_TRANSLATIONS,
|
|
|
|
lang
|
|
|
|
));
|
|
|
|
const challengeOrder = findIndex(
|
|
|
|
meta.challengeOrder,
|
|
|
|
([id]) => id === challenge.id
|
2018-11-18 18:04:04 +00:00
|
|
|
);
|
2020-08-28 15:10:37 +00:00
|
|
|
const {
|
|
|
|
name: blockName,
|
|
|
|
order,
|
|
|
|
superOrder,
|
|
|
|
isPrivate,
|
|
|
|
required = [],
|
|
|
|
template,
|
|
|
|
time
|
|
|
|
} = meta;
|
|
|
|
challenge.block = blockName;
|
2020-10-01 13:50:43 +00:00
|
|
|
challenge.dashedName =
|
|
|
|
lang === 'english'
|
|
|
|
? dasherize(challenge.title)
|
|
|
|
: dasherize(challenge.originalTitle);
|
|
|
|
delete challenge.originalTitle;
|
2020-08-28 15:10:37 +00:00
|
|
|
challenge.order = order;
|
|
|
|
challenge.superOrder = superOrder;
|
|
|
|
challenge.superBlock = superBlock;
|
|
|
|
challenge.challengeOrder = challengeOrder;
|
|
|
|
challenge.isPrivate = challenge.isPrivate || isPrivate;
|
|
|
|
challenge.required = required.concat(challenge.required || []);
|
|
|
|
challenge.template = template;
|
|
|
|
challenge.time = time;
|
2020-10-30 19:10:34 +00:00
|
|
|
challenge.helpCategory =
|
|
|
|
challenge.helpCategory || helpCategoryMap[dasherize(blockName)];
|
2018-11-16 18:22:52 +00:00
|
|
|
|
2020-08-28 15:10:37 +00:00
|
|
|
return prepareChallenge(challenge);
|
|
|
|
};
|
2020-06-08 13:01:48 +00:00
|
|
|
}
|
|
|
|
|
2020-06-12 12:47:58 +00:00
|
|
|
// TODO: tests and more descriptive name.
|
|
|
|
function filesToObject(files) {
|
|
|
|
return reduce(
|
|
|
|
files,
|
|
|
|
(map, file) => {
|
|
|
|
map[file.key] = {
|
|
|
|
...file,
|
|
|
|
head: arrToString(file.head),
|
|
|
|
contents: arrToString(file.contents),
|
|
|
|
tail: arrToString(file.tail)
|
|
|
|
};
|
|
|
|
return map;
|
|
|
|
},
|
|
|
|
{}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-06-08 13:01:48 +00:00
|
|
|
// gets the challenge ready for sourcing into Gatsby
|
|
|
|
function prepareChallenge(challenge) {
|
|
|
|
challenge.name = nameify(challenge.title);
|
|
|
|
if (challenge.files) {
|
2020-06-12 12:47:58 +00:00
|
|
|
challenge.files = filesToObject(challenge.files);
|
2020-06-08 13:01:48 +00:00
|
|
|
challenge.files = Object.keys(challenge.files)
|
|
|
|
.filter(key => challenge.files[key])
|
|
|
|
.map(key => challenge.files[key])
|
|
|
|
.reduce(
|
|
|
|
(files, file) => ({
|
|
|
|
...files,
|
|
|
|
[file.key]: {
|
|
|
|
...createPoly(file),
|
|
|
|
seed: file.contents.slice(0)
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
{}
|
|
|
|
);
|
|
|
|
}
|
2020-06-12 12:47:58 +00:00
|
|
|
|
|
|
|
if (challenge.solutionFiles) {
|
|
|
|
challenge.solutionFiles = filesToObject(challenge.solutionFiles);
|
|
|
|
}
|
2020-06-08 13:01:48 +00:00
|
|
|
challenge.block = dasherize(challenge.block);
|
|
|
|
challenge.superBlock = blockNameify(challenge.superBlock);
|
2018-11-16 18:22:52 +00:00
|
|
|
return challenge;
|
2018-10-04 13:45:22 +00:00
|
|
|
}
|
|
|
|
|
2020-08-28 15:10:37 +00:00
|
|
|
function hasEnglishSourceCreator(basePath) {
|
|
|
|
const englishRoot = path.resolve(__dirname, basePath, 'english');
|
|
|
|
return async function(translationPath) {
|
|
|
|
return await access(
|
|
|
|
path.join(englishRoot, translationPath),
|
|
|
|
fs.constants.F_OK
|
|
|
|
)
|
|
|
|
.then(() => true)
|
|
|
|
.catch(() => false);
|
|
|
|
};
|
2020-02-24 14:05:18 +00:00
|
|
|
}
|
|
|
|
|
2018-10-04 13:45:22 +00:00
|
|
|
function superBlockInfoFromPath(filePath) {
|
2018-10-25 09:54:57 +00:00
|
|
|
const [maybeSuper] = filePath.split(path.sep);
|
2018-10-04 13:45:22 +00:00
|
|
|
return superBlockInfo(maybeSuper);
|
2015-11-02 01:20:03 +00:00
|
|
|
}
|
|
|
|
|
2018-10-04 13:45:22 +00:00
|
|
|
function superBlockInfo(fileName) {
|
|
|
|
const [maybeOrder, ...superBlock] = fileName.split('-');
|
|
|
|
let order = parseInt(maybeOrder, 10);
|
2018-01-19 19:03:17 +00:00
|
|
|
if (isNaN(order)) {
|
2018-10-04 13:45:22 +00:00
|
|
|
return { order: 0, name: fileName };
|
2018-01-19 19:03:17 +00:00
|
|
|
} else {
|
|
|
|
return {
|
|
|
|
order: order,
|
2018-10-04 13:45:22 +00:00
|
|
|
name: superBlock.join('-')
|
2018-01-19 19:03:17 +00:00
|
|
|
};
|
2015-12-07 05:44:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-04 13:45:22 +00:00
|
|
|
function getBlockNameFromPath(filePath) {
|
2018-10-25 09:54:57 +00:00
|
|
|
const [, block] = filePath.split(path.sep);
|
2018-10-04 13:45:22 +00:00
|
|
|
return block;
|
|
|
|
}
|
2018-11-16 18:22:52 +00:00
|
|
|
|
2020-06-08 13:01:48 +00:00
|
|
|
function arrToString(arr) {
|
|
|
|
return Array.isArray(arr) ? arr.join('\n') : toString(arr);
|
|
|
|
}
|
2020-08-28 15:10:37 +00:00
|
|
|
|
|
|
|
exports.hasEnglishSourceCreator = hasEnglishSourceCreator;
|
|
|
|
exports.parseTranslation = parseTranslation;
|
|
|
|
exports.createChallengeCreator = createChallengeCreator;
|