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.
Format of translation unit:
+
+
+ <!--language-code-->
+
+ <!--title-->
+ Title
+ <!--end-->
+ <!--description-->
+ Description
+ <!--end-->
+ <!--end-->
+
'); - 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(''); 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('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('