258 lines
7.6 KiB
JavaScript
258 lines
7.6 KiB
JavaScript
const path = require('path');
|
|
const { findIndex } = require('lodash');
|
|
const readDirP = require('readdirp-walk');
|
|
const { parseMarkdown } = require('../tools/challenge-md-parser');
|
|
const fs = require('fs');
|
|
/* eslint-disable max-len */
|
|
const {
|
|
mergeChallenges,
|
|
translateCommentsInChallenge
|
|
} = require('../tools/challenge-md-parser/translation-parser/translation-parser');
|
|
/* eslint-enable max-len*/
|
|
const { COMMENT_TRANSLATIONS } = require('./comment-dictionary');
|
|
|
|
const { dasherize } = require('../utils/slugs');
|
|
const { isAuditedCert } = require('../utils/is-audited');
|
|
|
|
const challengesDir = path.resolve(__dirname, './challenges');
|
|
const metaDir = path.resolve(challengesDir, '_meta');
|
|
exports.challengesDir = challengesDir;
|
|
exports.metaDir = metaDir;
|
|
|
|
function getChallengesDirForLang(lang) {
|
|
return path.resolve(challengesDir, `./${lang}`);
|
|
}
|
|
|
|
function getMetaForBlock(block) {
|
|
return JSON.parse(
|
|
fs.readFileSync(path.resolve(metaDir, `./${block}/meta.json`), 'utf8')
|
|
);
|
|
}
|
|
|
|
exports.getChallengesDirForLang = getChallengesDirForLang;
|
|
exports.getMetaForBlock = getMetaForBlock;
|
|
|
|
exports.getChallengesForLang = function getChallengesForLang(lang) {
|
|
let curriculum = {};
|
|
return new Promise(resolve => {
|
|
let running = 1;
|
|
function done() {
|
|
if (--running === 0) {
|
|
resolve(curriculum);
|
|
}
|
|
}
|
|
readDirP({ root: getChallengesDirForLang(lang) })
|
|
.on('data', file => {
|
|
running++;
|
|
buildCurriculum(file, curriculum).then(done);
|
|
})
|
|
.on('end', done);
|
|
});
|
|
};
|
|
|
|
async function buildCurriculum(file, curriculum) {
|
|
const { name, depth, path: filePath, fullPath, stat } = file;
|
|
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()) {
|
|
const blockName = getBlockNameFromPath(filePath);
|
|
const metaPath = path.resolve(
|
|
__dirname,
|
|
`./challenges/_meta/${blockName}/meta.json`
|
|
);
|
|
const blockMeta = require(metaPath);
|
|
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;
|
|
}
|
|
return;
|
|
}
|
|
if (name === 'meta.json' || name === '.DS_Store') {
|
|
return;
|
|
}
|
|
|
|
const block = getBlockNameFromPath(filePath);
|
|
const { name: superBlock } = superBlockInfoFromPath(filePath);
|
|
let challengeBlock;
|
|
|
|
// TODO: this try block and process exit can all go once errors terminate the
|
|
// tests correctly.
|
|
try {
|
|
challengeBlock = curriculum[superBlock].blocks[block];
|
|
if (!challengeBlock) {
|
|
// this should only happen when a isUpcomingChange block is skipped
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
console.log(`failed to create superBlock ${superBlock}`);
|
|
// eslint-disable-next-line no-process-exit
|
|
process.exit(1);
|
|
}
|
|
const { meta } = challengeBlock;
|
|
|
|
const challenge = await createChallenge(fullPath, meta);
|
|
|
|
challengeBlock.challenges = [...challengeBlock.challenges, challenge];
|
|
}
|
|
|
|
async function parseTranslation(engPath, transPath, dict) {
|
|
const engChal = await parseMarkdown(engPath);
|
|
const translatedChal = await parseMarkdown(transPath);
|
|
const codeLang =
|
|
engChal.files && engChal.files[0] ? engChal.files[0].ext : null;
|
|
|
|
const engWithTranslatedComments = codeLang
|
|
? translateCommentsInChallenge(
|
|
engChal,
|
|
getChallengeLang(transPath),
|
|
dict,
|
|
codeLang
|
|
)
|
|
: engChal;
|
|
return mergeChallenges(engWithTranslatedComments, translatedChal);
|
|
}
|
|
|
|
exports.parseTranslation = parseTranslation;
|
|
|
|
async function createChallenge(fullPath, maybeMeta) {
|
|
let meta;
|
|
if (maybeMeta) {
|
|
meta = maybeMeta;
|
|
} else {
|
|
const metaPath = path.resolve(
|
|
metaDir,
|
|
`./${getBlockNameFromFullPath(fullPath)}/meta.json`
|
|
);
|
|
meta = require(metaPath);
|
|
}
|
|
const { name: superBlock } = superBlockInfoFromFullPath(fullPath);
|
|
const lang = getChallengeLang(fullPath);
|
|
if (!isAcceptedLanguage(lang))
|
|
throw Error(`${lang} is not a accepted language.
|
|
Trying to parse ${fullPath}`);
|
|
// 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 isEnglish =
|
|
isEnglishChallenge(fullPath) || !isAuditedCert(lang, superBlock);
|
|
if (isEnglish) fullPath = getEnglishPath(fullPath);
|
|
const challenge = await (isEnglish
|
|
? parseMarkdown(fullPath)
|
|
: parseTranslation(
|
|
getEnglishPath(fullPath),
|
|
fullPath,
|
|
COMMENT_TRANSLATIONS
|
|
));
|
|
const challengeOrder = findIndex(
|
|
meta.challengeOrder,
|
|
([id]) => id === challenge.id
|
|
);
|
|
const {
|
|
name: blockName,
|
|
order,
|
|
superOrder,
|
|
isPrivate,
|
|
required = [],
|
|
template,
|
|
time
|
|
} = meta;
|
|
challenge.block = blockName;
|
|
challenge.dashedName = dasherize(challenge.title);
|
|
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;
|
|
|
|
return challenge;
|
|
}
|
|
|
|
exports.createChallenge = createChallenge;
|
|
|
|
function getEnglishPath(fullPath) {
|
|
const posix = path
|
|
.normalize(fullPath)
|
|
.split(path.sep)
|
|
.join(path.posix.sep);
|
|
const match = posix.match(/(.*curriculum\/challenges\/)([^/]*)(.*)(\2)(.*)/);
|
|
const lang = getChallengeLang(fullPath);
|
|
if (!isAcceptedLanguage(lang))
|
|
throw Error(`${getChallengeLang(fullPath)} is not a accepted language.
|
|
Trying to parse ${fullPath}`);
|
|
if (match) {
|
|
return path.join(match[1], 'english', match[3] + 'english' + match[5]);
|
|
} else {
|
|
throw Error(`Malformed challenge path, ${fullPath} unable to parse.`);
|
|
}
|
|
}
|
|
|
|
function getChallengeLang(fullPath) {
|
|
const match = fullPath.match(/\.(\w+)\.md$/);
|
|
if (!match || match.length < 2)
|
|
throw Error(`Missing language extension for
|
|
${fullPath}`);
|
|
return fullPath.match(/\.(\w+)\.md$/)[1];
|
|
}
|
|
|
|
function isEnglishChallenge(fullPath) {
|
|
return getChallengeLang(fullPath) === 'english';
|
|
}
|
|
|
|
function isAcceptedLanguage(lang) {
|
|
const acceptedLanguages = ['english', 'chinese'];
|
|
return acceptedLanguages.includes(lang);
|
|
}
|
|
|
|
exports.getChallengeLang = getChallengeLang;
|
|
exports.getEnglishPath = getEnglishPath;
|
|
exports.isEnglishChallenge = isEnglishChallenge;
|
|
|
|
function superBlockInfoFromPath(filePath) {
|
|
const [maybeSuper] = filePath.split(path.sep);
|
|
return superBlockInfo(maybeSuper);
|
|
}
|
|
|
|
function superBlockInfoFromFullPath(fullFilePath) {
|
|
const [, , maybeSuper] = fullFilePath.split(path.sep).reverse();
|
|
return superBlockInfo(maybeSuper);
|
|
}
|
|
|
|
function superBlockInfo(fileName) {
|
|
const [maybeOrder, ...superBlock] = fileName.split('-');
|
|
let order = parseInt(maybeOrder, 10);
|
|
if (isNaN(order)) {
|
|
return { order: 0, name: fileName };
|
|
} else {
|
|
return {
|
|
order: order,
|
|
name: superBlock.join('-')
|
|
};
|
|
}
|
|
}
|
|
|
|
function getBlockNameFromPath(filePath) {
|
|
const [, block] = filePath.split(path.sep);
|
|
return block;
|
|
}
|
|
|
|
function getBlockNameFromFullPath(fullFilePath) {
|
|
const [, block] = fullFilePath.split(path.sep).reverse();
|
|
return block;
|
|
}
|