288 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			288 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
const path = require('path');
 | 
						|
const { findIndex, reduce, toString } = require('lodash');
 | 
						|
const readDirP = require('readdirp-walk');
 | 
						|
const { parseMarkdown } = require('../tools/challenge-md-parser');
 | 
						|
const fs = require('fs');
 | 
						|
const util = require('util');
 | 
						|
/* 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 { isAuditedCert } = require('../utils/is-audited');
 | 
						|
const { dasherize, nameify } = require('../utils/slugs');
 | 
						|
const { createPoly } = require('../utils/polyvinyl');
 | 
						|
const { blockNameify } = require('../utils/block-nameify');
 | 
						|
const { supportedLangs } = require('./utils');
 | 
						|
 | 
						|
const access = util.promisify(fs.access);
 | 
						|
 | 
						|
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, lang).then(done);
 | 
						|
      })
 | 
						|
      .on('end', done);
 | 
						|
  });
 | 
						|
};
 | 
						|
 | 
						|
async function buildCurriculum(file, curriculum, lang) {
 | 
						|
  const { name, depth, path: filePath, stat } = file;
 | 
						|
  const createChallenge = createChallengeCreator(challengesDir, lang);
 | 
						|
  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(filePath, meta);
 | 
						|
 | 
						|
  challengeBlock.challenges = [...challengeBlock.challenges, challenge];
 | 
						|
}
 | 
						|
 | 
						|
async function parseTranslation(engPath, transPath, dict, lang) {
 | 
						|
  const engChal = await parseMarkdown(engPath);
 | 
						|
  const translatedChal = await parseMarkdown(transPath);
 | 
						|
 | 
						|
  const engWithTranslatedComments = translateCommentsInChallenge(
 | 
						|
    engChal,
 | 
						|
    lang,
 | 
						|
    dict
 | 
						|
  );
 | 
						|
 | 
						|
  return mergeChallenges(engWithTranslatedComments, translatedChal);
 | 
						|
}
 | 
						|
 | 
						|
function createChallengeCreator(basePath, lang) {
 | 
						|
  const hasEnglishSource = hasEnglishSourceCreator(basePath);
 | 
						|
  return async function createChallenge(filePath, maybeMeta) {
 | 
						|
    function getFullPath(pathLang) {
 | 
						|
      return path.resolve(__dirname, basePath, pathLang, filePath);
 | 
						|
    }
 | 
						|
    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
 | 
						|
${getFullPath('english')}
 | 
						|
`);
 | 
						|
    // 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
 | 
						|
      ? parseMarkdown(getFullPath('english'))
 | 
						|
      : parseTranslation(
 | 
						|
          getFullPath('english'),
 | 
						|
          getFullPath(lang),
 | 
						|
          COMMENT_TRANSLATIONS,
 | 
						|
          lang
 | 
						|
        ));
 | 
						|
    const challengeOrder = findIndex(
 | 
						|
      meta.challengeOrder,
 | 
						|
      ([id]) => id === challenge.id
 | 
						|
    );
 | 
						|
    const {
 | 
						|
      name: blockName,
 | 
						|
      order,
 | 
						|
      superOrder,
 | 
						|
      isPrivate,
 | 
						|
      required = [],
 | 
						|
      template,
 | 
						|
      time
 | 
						|
    } = meta;
 | 
						|
    challenge.block = blockName;
 | 
						|
    challenge.dashedName =
 | 
						|
      lang === 'english'
 | 
						|
        ? dasherize(challenge.title)
 | 
						|
        : dasherize(challenge.originalTitle);
 | 
						|
    delete challenge.originalTitle;
 | 
						|
    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 prepareChallenge(challenge);
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
// 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;
 | 
						|
    },
 | 
						|
    {}
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
// gets the challenge ready for sourcing into Gatsby
 | 
						|
function prepareChallenge(challenge) {
 | 
						|
  challenge.name = nameify(challenge.title);
 | 
						|
  if (challenge.files) {
 | 
						|
    challenge.files = filesToObject(challenge.files);
 | 
						|
    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)
 | 
						|
          }
 | 
						|
        }),
 | 
						|
        {}
 | 
						|
      );
 | 
						|
  }
 | 
						|
 | 
						|
  if (challenge.solutionFiles) {
 | 
						|
    challenge.solutionFiles = filesToObject(challenge.solutionFiles);
 | 
						|
  }
 | 
						|
  challenge.block = dasherize(challenge.block);
 | 
						|
  challenge.superBlock = blockNameify(challenge.superBlock);
 | 
						|
  return challenge;
 | 
						|
}
 | 
						|
 | 
						|
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);
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
function superBlockInfoFromPath(filePath) {
 | 
						|
  const [maybeSuper] = filePath.split(path.sep);
 | 
						|
  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 arrToString(arr) {
 | 
						|
  return Array.isArray(arr) ? arr.join('\n') : toString(arr);
 | 
						|
}
 | 
						|
 | 
						|
exports.hasEnglishSourceCreator = hasEnglishSourceCreator;
 | 
						|
exports.parseTranslation = parseTranslation;
 | 
						|
exports.createChallengeCreator = createChallengeCreator;
 |