diff --git a/getChallenges.js b/getChallenges.js index 8eaa26fc79..b150e2d36d 100644 --- a/getChallenges.js +++ b/getChallenges.js @@ -8,21 +8,21 @@ const hiddenFile = /(^(\.|\/\.))|(.md$)/g; function getFilesFor(dir) { let targetDir = path.join(__dirname, dir); - return fs.readdirSync(targetDir) + return fs + .readdirSync(targetDir) .filter(file => !hiddenFile.test(file)) .map(function(file) { let superBlock; if (fs.statSync(path.join(targetDir, file)).isFile()) { - return {file: file}; + return { file: file }; } superBlock = file; - return getFilesFor(path.join(dir, superBlock)) - .map(function(data) { - return { - file: path.join(superBlock, data.file), - superBlock: superBlock - }; - }); + return getFilesFor(path.join(dir, superBlock)).map(function(data) { + return { + file: path.join(superBlock, data.file), + superBlock: superBlock + }; + }); }) .reduce(function(files, entry) { return files.concat(entry); @@ -33,7 +33,7 @@ function superblockInfo(filePath) { let parts = (filePath || '').split('-'); let order = parseInt(parts[0], 10); if (isNaN(order)) { - return {order: 0, name: filePath}; + return { order: 0, name: filePath }; } else { return { order: order, @@ -46,31 +46,26 @@ module.exports = function getChallenges(challengesDir) { if (!challengesDir) { challengesDir = 'challenges'; } - return getFilesFor(challengesDir) - .map(function(data) { - const challengeSpec = require('./' + challengesDir + '/' + data.file); - let superInfo = superblockInfo(data.superBlock); - challengeSpec.fileName = data.file; - challengeSpec.superBlock = superInfo.name; - challengeSpec.superOrder = superInfo.order; - challengeSpec.challenges = challengeSpec.challenges - .map(challenge => omit( - challenge, - [ - 'betaSolutions', - 'betaTests', - 'hints', - 'MDNlinks', - 'null', - 'rawSolutions', - 'react', - 'reactRedux', - 'redux', - 'releasedOn', - 'translations', - 'type' - ] - )); - return challengeSpec; - }); + return getFilesFor(challengesDir).map(function(data) { + const challengeSpec = require('./' + challengesDir + '/' + data.file); + let superInfo = superblockInfo(data.superBlock); + challengeSpec.fileName = data.file; + challengeSpec.superBlock = superInfo.name; + challengeSpec.superOrder = superInfo.order; + challengeSpec.challenges = challengeSpec.challenges.map(challenge => + omit(challenge, [ + 'betaSolutions', + 'betaTests', + 'hints', + 'MDNlinks', + 'null', + 'rawSolutions', + 'react', + 'reactRedux', + 'redux', + 'type' + ]) + ); + return challengeSpec; + }); }; diff --git a/schema/challengeSchema.js b/schema/challengeSchema.js index 7c86166873..de96a61575 100644 --- a/schema/challengeSchema.js +++ b/schema/challengeSchema.js @@ -4,12 +4,15 @@ Joi.objectId = require('joi-objectid')(Joi); const schema = Joi.object().keys({ block: Joi.string(), blockId: Joi.objectId(), - challengeType: Joi.number().min(0).max(9).required(), + challengeType: Joi.number() + .min(0) + .max(9) + .required(), checksum: Joi.number(), dashedName: Joi.string(), - description: Joi.array().items( - Joi.string().allow('') - ).required(), + description: Joi.array() + .items(Joi.string().allow('')) + .required(), fileName: Joi.string(), files: Joi.object().pattern( /(jsx?|html|css|sass)$/, @@ -17,14 +20,8 @@ const schema = Joi.object().keys({ key: Joi.string(), ext: Joi.string(), name: Joi.string(), - head: [ - Joi.array().items(Joi.string().allow('')), - Joi.string().allow('') - ], - tail: [ - Joi.array().items(Joi.string().allow('')), - Joi.string().allow('') - ], + head: [Joi.array().items(Joi.string().allow('')), Joi.string().allow('')], + tail: [Joi.array().items(Joi.string().allow('')), Joi.string().allow('')], contents: [ Joi.array().items(Joi.string().allow('')), Joi.string().allow('') @@ -49,9 +46,8 @@ const schema = Joi.object().keys({ crossDomain: Joi.bool() }) ), - solutions: Joi.array().items( - Joi.string().optional() - ), + releasedOn: Joi.string().allow(''), + solutions: Joi.array().items(Joi.string().optional()), superBlock: Joi.string(), superOrder: Joi.number(), suborder: Joi.number(), @@ -59,7 +55,9 @@ const schema = Joi.object().keys({ // public challenges Joi.object().keys({ text: Joi.string().required(), - testString: Joi.string().allow('').required() + testString: Joi.string() + .allow('') + .required() }), // our tests used in certification verification Joi.object().keys({ @@ -69,7 +67,14 @@ const schema = Joi.object().keys({ ), template: Joi.string(), time: Joi.string().allow(''), - title: Joi.string().required() + title: Joi.string().required(), + translations: Joi.object().pattern( + /\w+(-\w+)*/, + Joi.object().keys({ + title: Joi.string(), + description: Joi.array().items(Joi.string().allow('')) + }) + ) }); exports.validateChallenge = function validateChallenge(challenge) { diff --git a/unpackedChallenge.js b/unpackedChallenge.js index 6c273adf4c..ffa59af928 100644 --- a/unpackedChallenge.js +++ b/unpackedChallenge.js @@ -34,9 +34,6 @@ class ChallengeFile { // todo: make sure it works with encodings let data = fs.readFileSync(this.filePath()); let lines = data.toString().split(/(?:\r\n|\r|\n)/g); - let chunks = {}; - let readingChunk = null; - let currentParagraph = []; function removeLeadingEmptyLines(array) { let emptyString = /^\s*$/; @@ -45,69 +42,159 @@ class ChallengeFile { } } - lines.forEach(line => { - let chunkEnd = /(/; + + while (!endOfFile()) { + let line = nextLine(); + if (chunkEnd.test(line)) { + return translations; + } else if (langChunk.test(line)) { + let langCode = line.match(langChunk)[1]; + translations[langCode] = readProperties(); + line = nextLine(); + if (!chunkEnd.test(line)) { + throw `Expected --end--: + ${line}`; + } + } + } + throw `Unexpected end of the file while reading translations. + ${this.filePath()}`; + } + + function readFiles() { + let files = {}; + let fileChunk = //; + while (!endOfFile()) { + let line = nextLine(); + if (chunkEnd.test(line)) { + return files; + } else if (fileChunk.test(line)) { + let name = line.match(fileChunk)[1]; + let ext = line.match(fileChunk)[2]; + let key = name + ext; + files[key] = {}; + files[key].key = key; + files[key].ext = ext; + files[key].name = name; + Object.assign(files[key], readProperties()); + line = nextLine(); + if (!chunkEnd.test(line)) { + throw `Expected --end--: + ${line}`; + } + } + } + throw `Unexpected end of the file while reading files. + ${this.filePath()}`; + } + + function readProperty() { + let property = []; + while (!endOfFile()) { + let line = nextLine(); + if (chunkEnd.test(line)) { + removeLeadingEmptyLines(property); + return property; + } else if (line.startsWith(jsonLinePrefix)) { + line = JSON.parse(line.slice(jsonLinePrefix.length)); + property.push(line); + } else { + property.push(line); + } + } + throw `Unexpected end of the file while reading a property. + ${this.filePath()}`; + } + + function readProperties() { + let properties = {}; + let chunkStart = /( { - removeLeadingEmptyLines(chunks[key]); - }); - // console.log(JSON.stringify(chunks, null, 2)); return chunks; } } -export {ChallengeFile}; +export { ChallengeFile }; class UnpackedChallenge { constructor(targetDir, challengeJson, index) { @@ -130,8 +217,7 @@ class UnpackedChallenge { } unpack() { - this.challengeFile() - .write(this.unpackedHTML()); + this.challengeFile().write(this.unpackedHTML()); } challengeFile() { @@ -140,14 +226,14 @@ class UnpackedChallenge { baseName() { // eslint-disable-next-line no-nested-ternary - let prefix = ((this.index < 10) ? '00' : (this.index < 100) ? '0' : '') - + this.index; + let prefix = + (this.index < 10 ? '00' : this.index < 100 ? '0' : '') + this.index; return `${prefix}-${dasherize(this.challenge.title)}-${this.challenge.id}`; } - expandedDescription() { + expandedDescription(description) { let out = []; - this.challenge.description.forEach(part => { + description.forEach(part => { if (_.isString(part)) { out.push(part.toString()); out.push(paragraphBreak); @@ -167,7 +253,7 @@ class UnpackedChallenge { } }); - if (out[ out.length - 1 ] === paragraphBreak) { + if (out[out.length - 1] === paragraphBreak) { out.pop(); } return out; @@ -208,42 +294,138 @@ class UnpackedChallenge { and run npm run repack to incorporate your changes into the challenge database.

`); + text.push('

Title

'); + text.push(''); + text.push(this.challenge.title); + text.push(''); + text.push(''); text.push('

Description

'); text.push('
'); text.push(''); if (this.challenge.description.length) { - text.push(this.expandedDescription().join('\n')); + text.push( + this.expandedDescription(this.challenge.description).join('\n') + ); + } + text.push(''); + text.push('
'); + text.push(''); + text.push('

Translations

'); + text.push(` +

Format of translation unit:

+

+ + <!--language-code-->
+ <!--title-->
+ Title
+ <!--end-->
+ <!--description-->
+ Description
+ <!--end-->
+ <!--end-->
+
+

`); + text.push('
'); + text.push(''); + if (this.challenge.hasOwnProperty('translations')) { + const translations = this.challenge.translations; + const keys = Object.keys(translations); + if (keys) { + keys.forEach(lang => { + text.push(`

${lang}

`); + text.push(``); + const translation = translations[lang]; + if (translation.title) { + text.push('

Title

'); + text.push(''); + text.push(translation.title); + text.push(''); + } + if (translation.description && translation.description.length) { + text.push('

Description

'); + text.push(''); + text.push( + this.expandedDescription(translation.description).join('\n') + ); + text.push(''); + } + text.push(''); + }); + } } text.push(''); text.push('
'); text.push(''); - text.push('

Seed

'); - text.push('
');
-    if (this.challenge.seed) {
-      text.push(text, this.challenge.seed.join('\n'));
-    }
+    text.push('

Released On

'); + text.push('
'); + text.push(''); + text.push(this.challenge.releasedOn); text.push(''); - text.push('
'); - - // Q: What is the difference between 'seed' and 'challengeSeed' ? - text.push(''); - text.push('

Challenge Seed

'); - text.push('
');
-    if (this.challenge.challengeSeed) {
-      text.push(text, this.challenge.challengeSeed.join('\n'));
-    }
-    text.push('');
-    text.push('
'); + text.push(''); text.push(''); - text.push('

Head

'); - text.push(''); + text.push(''); + text.push(''); text.push(''); text.push('

Solution

'); @@ -253,29 +435,24 @@ class UnpackedChallenge { // Note: none of the challenges have more than one solution // todo: should we deal with multiple solutions or not? if (this.challenge.solutions && this.challenge.solutions.length > 0) { - let solution = this.challenge.solutions[ 0 ]; + let solution = this.challenge.solutions[0]; text.push(solution); } text.push(''); - text.push(''); - text.push('

Tail

'); - text.push(''); - text.push(''); text.push('

Tests

'); text.push('