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(`^
`); 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
${description}
## Instructions
${instructions}
## Tests
\`\`\`yml ${tests} \`\`\`
${ ext ? `## Challenge Seed
\`\`\`${ext} ${contents} \`\`\`
${ head ? ` ### Before Tests
\`\`\`${ext} ${head} \`\`\`
` : '' }${ tail ? ` ### After Tests
\`\`\`${ext} ${tail} \`\`\`
` : '' }
## Solution ${solutions.reduce( (result, solution) => result + `
\`\`\`${ext} ${solution.trim()} \`\`\`
`, '' )} ` : '' }`; }; }