398 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			398 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| const unified = require('unified');
 | |
| const markdown = require('remark-parse');
 | |
| const remark2rehype = require('remark-rehype');
 | |
| const stringify = require('remark-stringify');
 | |
| const frontmatter = require('remark-frontmatter');
 | |
| const raw = require('rehype-raw');
 | |
| const visit = require('unist-util-visit');
 | |
| const vfile = require('to-vfile');
 | |
| const path = require('path');
 | |
| const { Transform } = require('stream');
 | |
| const { Translate } = require('@google-cloud/translate');
 | |
| const YAML = require('js-yaml');
 | |
| 
 | |
| const solutionsToData = require('./solution-to-data');
 | |
| const challengeSeedToData = require('./challengeSeed-to-data');
 | |
| 
 | |
| const transformChallenge = new Transform({
 | |
|   transform(chunk, encoding, callback) {
 | |
|     const fileName = chunk.toString().trim();
 | |
| 
 | |
|     rebuildChallengeFile(fileName)
 | |
|       .then(file => callback(null, String(file.contents)))
 | |
|       .catch(err => console.error(err));
 | |
|   }
 | |
| });
 | |
| 
 | |
| process.stdin.pipe(transformChallenge).pipe(process.stdout);
 | |
| 
 | |
| const processor = unified()
 | |
|   .use(markdown)
 | |
|   .use(frontmatter, ['yaml'])
 | |
|   .use(frontmatterToData)
 | |
|   .use(testsToData)
 | |
|   .use(textToData, ['description', 'instructions'])
 | |
|   .use(remark2rehype, { allowDangerousHTML: true })
 | |
|   .use(raw)
 | |
|   .use(solutionsToData)
 | |
|   .use(challengeSeedToData)
 | |
|   .use(replaceWithReferenceData)
 | |
|   .use(output);
 | |
| 
 | |
| exports.rebuildChallengeFile = rebuildChallengeFile;
 | |
| 
 | |
| async function rebuildChallengeFile(fileName) {
 | |
|   const filePath = path.resolve(fileName);
 | |
|   const lang = detectLang(filePath);
 | |
|   let referenceChallenge;
 | |
|   let translateText;
 | |
|   if (lang !== 'english') {
 | |
|     referenceChallenge = await getReferenceChallengeData(filePath);
 | |
|     translateText = createTranslateText(lang);
 | |
|   }
 | |
| 
 | |
|   const file = await vfile.read(filePath);
 | |
|   file.data = {
 | |
|     ...file.data,
 | |
|     lang,
 | |
|     referenceChallenge,
 | |
|     translateText
 | |
|   };
 | |
|   return await processor.process(file);
 | |
| }
 | |
| 
 | |
| async function getReferenceChallengeData(filePath) {
 | |
|   const parts = filePath.split(path.sep);
 | |
|   parts.push(parts.pop().replace(/\.[^.]+\.md$/, '.english.md'));
 | |
|   parts[parts.length - 4] = 'english';
 | |
|   const filePathEnglishChallenge = parts.join(path.sep);
 | |
|   try {
 | |
|     const fileData = await vfile.read(filePathEnglishChallenge);
 | |
|     fileData.data = { ...fileData.data, refData: true };
 | |
|     return (await processor.process(fileData)).data;
 | |
|   } catch (err) {
 | |
|     console.error(err);
 | |
|     return null;
 | |
|   }
 | |
| }
 | |
| 
 | |
| function detectLang(filePath) {
 | |
|   const match = /\.([^.]+)\.md$/.exec(filePath);
 | |
|   if (!match) {
 | |
|     throw new Error(`Incorrect file path ${filePath}`);
 | |
|   }
 | |
|   return match[1];
 | |
| }
 | |
| 
 | |
| function frontmatterToData() {
 | |
|   return transformer;
 | |
| 
 | |
|   function transformer(tree, file) {
 | |
|     visit(tree, 'yaml', visitor);
 | |
| 
 | |
|     function visitor(node) {
 | |
|       const frontmatter = node.value;
 | |
| 
 | |
|       file.data = { ...file.data, frontmatter };
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| function testsToData() {
 | |
|   return (tree, file) => {
 | |
|     visit(tree, 'code', visitor);
 | |
| 
 | |
|     function visitor(node) {
 | |
|       const { lang, value } = node;
 | |
|       if (lang === 'yml') {
 | |
|         file.data = {
 | |
|           ...file.data,
 | |
|           tests: value
 | |
|         };
 | |
|       }
 | |
|     }
 | |
|   };
 | |
| }
 | |
| 
 | |
| function textToData(sectionIds) {
 | |
|   return (tree, file) => {
 | |
|     let indexId = 0;
 | |
|     let currentSection = sectionIds[indexId];
 | |
|     let inSection = false;
 | |
|     let nodes = [];
 | |
|     let findSection;
 | |
|     const visitor = (node, index, parent) => {
 | |
|       if (!parent) {
 | |
|         return visit.CONTINUE;
 | |
|       }
 | |
| 
 | |
|       if (node.type === 'heading') {
 | |
|         if (inSection) {
 | |
|           findSection = new RegExp(`^<section id=('|")${currentSection}\\1>`);
 | |
|           file.data = {
 | |
|             ...file.data,
 | |
|             [currentSection]: new stringify.Compiler(
 | |
|               { type: 'root', children: nodes },
 | |
|               file
 | |
|             )
 | |
|               .compile()
 | |
|               .trim()
 | |
|               .replace(findSection, '')
 | |
|               .replace(/<\/section>$/, '')
 | |
|               .trim()
 | |
|           };
 | |
|           nodes = [];
 | |
|           indexId++;
 | |
|           if (indexId < sectionIds.length) {
 | |
|             currentSection = sectionIds[indexId];
 | |
|           } else {
 | |
|             return visit.EXIT;
 | |
|           }
 | |
|         }
 | |
|         inSection = true;
 | |
|       } else if (inSection) {
 | |
|         nodes.push(node);
 | |
|       }
 | |
| 
 | |
|       return visit.SKIP;
 | |
|     };
 | |
|     visit(tree, visitor);
 | |
|   };
 | |
| }
 | |
| 
 | |
| function createTranslateText(target, source = 'english') {
 | |
|   const projectId = process.env.GOOGLE_CLOUD_PROJECT_ID;
 | |
|   if (!projectId) {
 | |
|     return async text => text;
 | |
|   }
 | |
|   const languageCodes = {
 | |
|     arabic: 'ar',
 | |
|     chinese: 'zh',
 | |
|     english: 'en',
 | |
|     portuguese: 'pt',
 | |
|     russian: 'ru',
 | |
|     spanish: 'es'
 | |
|   };
 | |
|   const from = languageCodes[source];
 | |
|   const to = languageCodes[target];
 | |
|   return async text => {
 | |
|     if (!text) {
 | |
|       return text;
 | |
|     }
 | |
|     try {
 | |
|       const translate = new Translate({ projectId });
 | |
|       const result = await translate.translate(text, { from, to });
 | |
|       const translations = result[0];
 | |
|       return translations;
 | |
|     } catch (err) {
 | |
|       // console.error(err);
 | |
|       return text;
 | |
|     }
 | |
|   };
 | |
| }
 | |
| 
 | |
| async function processTests(tests, referenceTests, translateText) {
 | |
|   const testsObject = YAML.load(referenceTests);
 | |
|   if (
 | |
|     !testsObject.tests ||
 | |
|     testsObject.tests.length === 0 ||
 | |
|     !testsObject.tests[0].text
 | |
|   ) {
 | |
|     return referenceTests;
 | |
|   }
 | |
|   const newTests = await Promise.all(
 | |
|     testsObject.tests.map(async test => {
 | |
|       const text = await translateText(test.text);
 | |
|       return { ...test, text };
 | |
|     })
 | |
|   );
 | |
|   const testStrings = newTests
 | |
|     .map(
 | |
|       ({ text, testString }) =>
 | |
|         `  - text: ${dumpToYamlString(text)}    testString: ${dumpToYamlString(
 | |
|           testString
 | |
|         )}`
 | |
|     )
 | |
|     .join('');
 | |
|   return `tests:${testStrings ? '\n' + testStrings : ' []\n'}`;
 | |
| }
 | |
| 
 | |
| function dumpToYamlString(text) {
 | |
|   let fromYaml;
 | |
|   try {
 | |
|     fromYaml = YAML.load(text);
 | |
|   } catch {
 | |
|     // console.error(`YAML load: ${text}`);
 | |
|   }
 | |
|   if (text === fromYaml) {
 | |
|     return text + '\n';
 | |
|   }
 | |
|   return YAML.dump(text, { lineWidth: 10000 });
 | |
| }
 | |
| 
 | |
| async function processFrontmatter(fileData) {
 | |
|   const { referenceChallenge, lang, translateText } = fileData;
 | |
|   const challengeData = YAML.load(fileData.frontmatter);
 | |
|   let data;
 | |
|   if (referenceChallenge) {
 | |
|     data = YAML.load(referenceChallenge.frontmatter);
 | |
|   } else {
 | |
|     data = challengeData;
 | |
|   }
 | |
| 
 | |
|   if (lang && lang !== 'english') {
 | |
|     if (challengeData.localeTitle) {
 | |
|       data.localeTitle = challengeData.localeTitle;
 | |
|     } else {
 | |
|       data.localeTitle = await translateText(data.title);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   fileData.frontmatter = Object.entries(data)
 | |
|     .map(([name, value]) => {
 | |
|       if (typeof value === 'object') {
 | |
|         return `${name}:
 | |
|   ${dumpToYamlString(value)
 | |
|     .replace(/\n/, '\n  ')
 | |
|     .trimRight()}
 | |
| `;
 | |
|       }
 | |
|       return `${name}: ${dumpToYamlString(value)}`;
 | |
|     })
 | |
|     .join('')
 | |
|     .trimRight();
 | |
| }
 | |
| 
 | |
| function replaceWithReferenceData() {
 | |
|   return async (tree, file) => {
 | |
|     if (file.data.refData) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const { referenceChallenge, translateText } = file.data;
 | |
| 
 | |
|     await processFrontmatter(file.data);
 | |
| 
 | |
|     if (referenceChallenge) {
 | |
|       const { description, instructions } = file.data;
 | |
| 
 | |
|       if (!description || description === 'undefined') {
 | |
|         file.data.description = await translateText(
 | |
|           referenceChallenge.description
 | |
|         );
 | |
|       }
 | |
|       if (!instructions || instructions === 'undefined') {
 | |
|         file.data.instructions = await translateText(
 | |
|           referenceChallenge.instructions
 | |
|         );
 | |
|       }
 | |
|       file.data.tests = await processTests(
 | |
|         file.data.tests,
 | |
|         referenceChallenge.tests,
 | |
|         translateText
 | |
|       );
 | |
|       file.data.files = referenceChallenge.files;
 | |
|       file.data.solutions = referenceChallenge.solutions;
 | |
|     }
 | |
|   };
 | |
| }
 | |
| 
 | |
| function output() {
 | |
|   this.Compiler = function(tree, file) {
 | |
|     let {
 | |
|       frontmatter,
 | |
|       description,
 | |
|       instructions,
 | |
|       tests,
 | |
|       files: [challengeFile = {}]
 | |
|     } = file.data;
 | |
|     const { ext, contents, head, tail } = challengeFile;
 | |
|     let { solutions = [] } = file.data;
 | |
|     solutions = solutions
 | |
|       .map(s => s.trim())
 | |
|       .map(s =>
 | |
|         !s.includes('\n') && /^\/\//.test(s) ? '// solution required' : s
 | |
|       );
 | |
|     return `---
 | |
| ${frontmatter}
 | |
| ---
 | |
| 
 | |
| ## Description
 | |
| <section id='description'>
 | |
| ${description}
 | |
| </section>
 | |
| 
 | |
| ## Instructions
 | |
| <section id='instructions'>
 | |
| ${instructions}
 | |
| </section>
 | |
| 
 | |
| ## Tests
 | |
| <section id='tests'>
 | |
| 
 | |
| \`\`\`yml
 | |
| ${tests}
 | |
| \`\`\`
 | |
| 
 | |
| </section>
 | |
| 
 | |
| ${
 | |
|   ext
 | |
|     ? `## Challenge Seed
 | |
| <section id='challengeSeed'>
 | |
| 
 | |
| <div id='${ext}-seed'>
 | |
| 
 | |
| \`\`\`${ext}
 | |
| ${contents}
 | |
| \`\`\`
 | |
| 
 | |
| </div>${
 | |
|         head
 | |
|           ? `
 | |
| 
 | |
| ### Before Tests
 | |
| <div id='${ext}-setup'>
 | |
| 
 | |
| \`\`\`${ext}
 | |
| ${head}
 | |
| \`\`\`
 | |
| 
 | |
| </div>`
 | |
|           : ''
 | |
|       }${
 | |
|         tail
 | |
|           ? `
 | |
| 
 | |
| ### After Tests
 | |
| <div id='${ext}-teardown'>
 | |
| 
 | |
| \`\`\`${ext}
 | |
| ${tail}
 | |
| \`\`\`
 | |
| 
 | |
| </div>`
 | |
|           : ''
 | |
|       }
 | |
| 
 | |
| </section>
 | |
| 
 | |
| ## Solution
 | |
| ${solutions.reduce(
 | |
|   (result, solution) =>
 | |
|     result +
 | |
|     `<section id='solution'>
 | |
| 
 | |
| \`\`\`${ext}
 | |
| ${solution.trim()}
 | |
| \`\`\`
 | |
| 
 | |
| </section>`,
 | |
|   ''
 | |
| )}
 | |
| `
 | |
|     : ''
 | |
| }`;
 | |
|   };
 | |
| }
 |