fix(scripts): fix unpack and repack scripts for the new challenge schema

This commit is contained in:
ValeraS
2018-07-10 17:21:07 +03:00
committed by Kristofer Koishigawa
parent 2edb306ca1
commit 52ed7cfa7e
3 changed files with 319 additions and 143 deletions

View File

@ -8,21 +8,21 @@ const hiddenFile = /(^(\.|\/\.))|(.md$)/g;
function getFilesFor(dir) { function getFilesFor(dir) {
let targetDir = path.join(__dirname, dir); let targetDir = path.join(__dirname, dir);
return fs.readdirSync(targetDir) return fs
.readdirSync(targetDir)
.filter(file => !hiddenFile.test(file)) .filter(file => !hiddenFile.test(file))
.map(function(file) { .map(function(file) {
let superBlock; let superBlock;
if (fs.statSync(path.join(targetDir, file)).isFile()) { if (fs.statSync(path.join(targetDir, file)).isFile()) {
return {file: file}; return { file: file };
} }
superBlock = file; superBlock = file;
return getFilesFor(path.join(dir, superBlock)) return getFilesFor(path.join(dir, superBlock)).map(function(data) {
.map(function(data) { return {
return { file: path.join(superBlock, data.file),
file: path.join(superBlock, data.file), superBlock: superBlock
superBlock: superBlock };
}; });
});
}) })
.reduce(function(files, entry) { .reduce(function(files, entry) {
return files.concat(entry); return files.concat(entry);
@ -33,7 +33,7 @@ function superblockInfo(filePath) {
let parts = (filePath || '').split('-'); let parts = (filePath || '').split('-');
let order = parseInt(parts[0], 10); let order = parseInt(parts[0], 10);
if (isNaN(order)) { if (isNaN(order)) {
return {order: 0, name: filePath}; return { order: 0, name: filePath };
} else { } else {
return { return {
order: order, order: order,
@ -46,31 +46,26 @@ module.exports = function getChallenges(challengesDir) {
if (!challengesDir) { if (!challengesDir) {
challengesDir = 'challenges'; challengesDir = 'challenges';
} }
return getFilesFor(challengesDir) return getFilesFor(challengesDir).map(function(data) {
.map(function(data) { const challengeSpec = require('./' + challengesDir + '/' + data.file);
const challengeSpec = require('./' + challengesDir + '/' + data.file); let superInfo = superblockInfo(data.superBlock);
let superInfo = superblockInfo(data.superBlock); challengeSpec.fileName = data.file;
challengeSpec.fileName = data.file; challengeSpec.superBlock = superInfo.name;
challengeSpec.superBlock = superInfo.name; challengeSpec.superOrder = superInfo.order;
challengeSpec.superOrder = superInfo.order; challengeSpec.challenges = challengeSpec.challenges.map(challenge =>
challengeSpec.challenges = challengeSpec.challenges omit(challenge, [
.map(challenge => omit( 'betaSolutions',
challenge, 'betaTests',
[ 'hints',
'betaSolutions', 'MDNlinks',
'betaTests', 'null',
'hints', 'rawSolutions',
'MDNlinks', 'react',
'null', 'reactRedux',
'rawSolutions', 'redux',
'react', 'type'
'reactRedux', ])
'redux', );
'releasedOn', return challengeSpec;
'translations', });
'type'
]
));
return challengeSpec;
});
}; };

View File

@ -4,12 +4,15 @@ Joi.objectId = require('joi-objectid')(Joi);
const schema = Joi.object().keys({ const schema = Joi.object().keys({
block: Joi.string(), block: Joi.string(),
blockId: Joi.objectId(), blockId: Joi.objectId(),
challengeType: Joi.number().min(0).max(9).required(), challengeType: Joi.number()
.min(0)
.max(9)
.required(),
checksum: Joi.number(), checksum: Joi.number(),
dashedName: Joi.string(), dashedName: Joi.string(),
description: Joi.array().items( description: Joi.array()
Joi.string().allow('') .items(Joi.string().allow(''))
).required(), .required(),
fileName: Joi.string(), fileName: Joi.string(),
files: Joi.object().pattern( files: Joi.object().pattern(
/(jsx?|html|css|sass)$/, /(jsx?|html|css|sass)$/,
@ -17,14 +20,8 @@ const schema = Joi.object().keys({
key: Joi.string(), key: Joi.string(),
ext: Joi.string(), ext: Joi.string(),
name: Joi.string(), name: Joi.string(),
head: [ head: [Joi.array().items(Joi.string().allow('')), Joi.string().allow('')],
Joi.array().items(Joi.string().allow('')), tail: [Joi.array().items(Joi.string().allow('')), Joi.string().allow('')],
Joi.string().allow('')
],
tail: [
Joi.array().items(Joi.string().allow('')),
Joi.string().allow('')
],
contents: [ contents: [
Joi.array().items(Joi.string().allow('')), Joi.array().items(Joi.string().allow('')),
Joi.string().allow('') Joi.string().allow('')
@ -49,9 +46,8 @@ const schema = Joi.object().keys({
crossDomain: Joi.bool() crossDomain: Joi.bool()
}) })
), ),
solutions: Joi.array().items( releasedOn: Joi.string().allow(''),
Joi.string().optional() solutions: Joi.array().items(Joi.string().optional()),
),
superBlock: Joi.string(), superBlock: Joi.string(),
superOrder: Joi.number(), superOrder: Joi.number(),
suborder: Joi.number(), suborder: Joi.number(),
@ -59,7 +55,9 @@ const schema = Joi.object().keys({
// public challenges // public challenges
Joi.object().keys({ Joi.object().keys({
text: Joi.string().required(), text: Joi.string().required(),
testString: Joi.string().allow('').required() testString: Joi.string()
.allow('')
.required()
}), }),
// our tests used in certification verification // our tests used in certification verification
Joi.object().keys({ Joi.object().keys({
@ -69,7 +67,14 @@ const schema = Joi.object().keys({
), ),
template: Joi.string(), template: Joi.string(),
time: Joi.string().allow(''), 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) { exports.validateChallenge = function validateChallenge(challenge) {

View File

@ -34,9 +34,6 @@ class ChallengeFile {
// todo: make sure it works with encodings // todo: make sure it works with encodings
let data = fs.readFileSync(this.filePath()); let data = fs.readFileSync(this.filePath());
let lines = data.toString().split(/(?:\r\n|\r|\n)/g); let lines = data.toString().split(/(?:\r\n|\r|\n)/g);
let chunks = {};
let readingChunk = null;
let currentParagraph = [];
function removeLeadingEmptyLines(array) { function removeLeadingEmptyLines(array) {
let emptyString = /^\s*$/; let emptyString = /^\s*$/;
@ -45,69 +42,159 @@ class ChallengeFile {
} }
} }
lines.forEach(line => { let chunkEnd = /(<!|\/\*)--end--/;
let chunkEnd = /(<!|\/\*)--end--/; let index = 0;
let chunkStart = /(<!|\/\*)--(\w+)--/;
line = line.toString(); function endOfFile() {
return index === lines.length;
}
function nextLine() {
if (endOfFile()) {
return null;
}
return lines[index++].toString();
}
function readDescription() {
let description = [];
let currentParagraph = [];
function pushParagraph() { function pushParagraph() {
removeLeadingEmptyLines(currentParagraph); removeLeadingEmptyLines(currentParagraph);
chunks[ readingChunk ].push(currentParagraph.join('\n')); description.push(currentParagraph.join('\n'));
currentParagraph = []; currentParagraph = [];
} }
if (chunkEnd.test(line)) { while (!endOfFile()) {
if (!readingChunk) { let line = nextLine();
throw 'Encountered --end-- without being in a chunk'; if (chunkEnd.test(line)) {
} pushParagraph();
if (currentParagraph.length) { return description;
} else if (line === paragraphBreak) {
pushParagraph(); pushParagraph();
} else { } else {
removeLeadingEmptyLines(chunks[readingChunk]);
}
readingChunk = null;
} else if (readingChunk === 'description' && line === paragraphBreak) {
pushParagraph();
} else if (chunkStart.test(line)) {
let chunkName = line.match(chunkStart)[ 2 ];
if (readingChunk) {
throw `Encountered chunk ${chunkName} start `
+ `while already reading ${readingChunk}:
${line}`;
}
readingChunk = chunkName;
} else if (readingChunk) {
if (!chunks[ readingChunk ]) {
chunks[ readingChunk ] = [];
}
if (line.startsWith(jsonLinePrefix)) {
line = JSON.parse(line.slice(jsonLinePrefix.length));
chunks[ readingChunk ].push(line);
} else if (readingChunk === 'description') {
currentParagraph.push(line); currentParagraph.push(line);
} else {
chunks[ readingChunk ].push(line);
} }
} }
}); throw `Unexpected end of the file while reading a description.
${this.filePath()}`;
}
function readTranslations() {
let translations = {};
let langChunk = /<!--(\w+(-\w+)*)-->/;
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 = /<!--(\w+)\.(\w+)-->/;
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 = /(<!|\/\*)--(\w+)--/;
while (!endOfFile()) {
let line = nextLine();
if (chunkEnd.test(line)) {
index--; // must to do step back
return properties;
} else if (chunkStart.test(line)) {
let chunkName = line.match(chunkStart)[2];
if (chunkName === 'description') {
properties[chunkName] = readDescription();
} else if (chunkName === 'translations') {
properties[chunkName] = readTranslations();
} else if (chunkName === 'files') {
properties[chunkName] = readFiles();
} else {
properties[chunkName] = readProperty();
if (chunkName === 'releasedOn' || chunkName === 'title') {
properties[chunkName] = properties[chunkName].join('\n');
}
}
}
}
return properties;
}
let chunks = readProperties();
if (!endOfFile()) {
throw `Unexpected content of file: ${this.filePath()}`;
}
// hack to deal with solutions field being an array of a single string // hack to deal with solutions field being an array of a single string
// instead of an array of lines like some other fields // instead of an array of lines like some other fields
if (chunks.solutions) { if (chunks.solutions) {
chunks.solutions = [ chunks.solutions.join('\n') ]; chunks.solutions = [chunks.solutions.join('\n')];
removeLeadingEmptyLines(chunks.solutions);
} }
Object.keys(chunks).forEach(key => {
removeLeadingEmptyLines(chunks[key]);
});
// console.log(JSON.stringify(chunks, null, 2)); // console.log(JSON.stringify(chunks, null, 2));
return chunks; return chunks;
} }
} }
export {ChallengeFile}; export { ChallengeFile };
class UnpackedChallenge { class UnpackedChallenge {
constructor(targetDir, challengeJson, index) { constructor(targetDir, challengeJson, index) {
@ -130,8 +217,7 @@ class UnpackedChallenge {
} }
unpack() { unpack() {
this.challengeFile() this.challengeFile().write(this.unpackedHTML());
.write(this.unpackedHTML());
} }
challengeFile() { challengeFile() {
@ -140,14 +226,14 @@ class UnpackedChallenge {
baseName() { baseName() {
// eslint-disable-next-line no-nested-ternary // eslint-disable-next-line no-nested-ternary
let prefix = ((this.index < 10) ? '00' : (this.index < 100) ? '0' : '') let prefix =
+ this.index; (this.index < 10 ? '00' : this.index < 100 ? '0' : '') + this.index;
return `${prefix}-${dasherize(this.challenge.title)}-${this.challenge.id}`; return `${prefix}-${dasherize(this.challenge.title)}-${this.challenge.id}`;
} }
expandedDescription() { expandedDescription(description) {
let out = []; let out = [];
this.challenge.description.forEach(part => { description.forEach(part => {
if (_.isString(part)) { if (_.isString(part)) {
out.push(part.toString()); out.push(part.toString());
out.push(paragraphBreak); out.push(paragraphBreak);
@ -167,7 +253,7 @@ class UnpackedChallenge {
} }
}); });
if (out[ out.length - 1 ] === paragraphBreak) { if (out[out.length - 1] === paragraphBreak) {
out.pop(); out.pop();
} }
return out; return out;
@ -208,42 +294,138 @@ class UnpackedChallenge {
and run <code>npm run repack</code> and run <code>npm run repack</code>
to incorporate your changes into the challenge database.</p>`); to incorporate your changes into the challenge database.</p>`);
text.push('<h2>Title</h2>');
text.push('<!--title-->');
text.push(this.challenge.title);
text.push('<!--end-->');
text.push(''); text.push('');
text.push('<h2>Description</h2>'); text.push('<h2>Description</h2>');
text.push('<div class="unpacked description">'); text.push('<div class="unpacked description">');
text.push('<!--description-->'); text.push('<!--description-->');
if (this.challenge.description.length) { if (this.challenge.description.length) {
text.push(this.expandedDescription().join('\n')); text.push(
this.expandedDescription(this.challenge.description).join('\n')
);
}
text.push('<!--end-->');
text.push('</div>');
text.push('');
text.push('<h2>Translations</h2>');
text.push(`
<p>Format of translation unit:</p>
<p>
<code>
&lt!--<em>language-code</em>--&gt<br>
&lt!--<em>title</em>--&gt<br>
<em>Title</em><br>
&lt!--<em>end</em>--&gt<br>
&lt!--<em>description</em>--&gt<br>
<em>Description</em><br>
&lt!--<em>end</em>--&gt<br>
&lt!--<em>end</em>--&gt<br>
</code>
</p>`);
text.push('<div class="unpacked">');
text.push('<!--translations-->');
if (this.challenge.hasOwnProperty('translations')) {
const translations = this.challenge.translations;
const keys = Object.keys(translations);
if (keys) {
keys.forEach(lang => {
text.push(`<h2>${lang}</h2>`);
text.push(`<!--${lang}-->`);
const translation = translations[lang];
if (translation.title) {
text.push('<h3>Title</h3>');
text.push('<!--title-->');
text.push(translation.title);
text.push('<!--end-->');
}
if (translation.description && translation.description.length) {
text.push('<h3>Description</h3>');
text.push('<!--description-->');
text.push(
this.expandedDescription(translation.description).join('\n')
);
text.push('<!--end-->');
}
text.push('<!--end-->');
});
}
} }
text.push('<!--end-->'); text.push('<!--end-->');
text.push('</div>'); text.push('</div>');
text.push(''); text.push('');
text.push('<h2>Seed</h2>'); text.push('<h2>Released On</h2>');
text.push('<!--seed--><pre class="unpacked">'); text.push('<div class="unpacked">');
if (this.challenge.seed) { text.push('<!--releasedOn-->');
text.push(text, this.challenge.seed.join('\n')); text.push(this.challenge.releasedOn);
}
text.push('<!--end-->'); text.push('<!--end-->');
text.push('</pre>'); text.push('</div>');
// Q: What is the difference between 'seed' and 'challengeSeed' ?
text.push('');
text.push('<h2>Challenge Seed</h2>');
text.push('<!--challengeSeed--><pre class="unpacked">');
if (this.challenge.challengeSeed) {
text.push(text, this.challenge.challengeSeed.join('\n'));
}
text.push('<!--end-->');
text.push('</pre>');
text.push(''); text.push('');
text.push('<h2>Head</h2>'); text.push('<h2>Files</h2>');
text.push('<!--head--><script class="unpacked head">'); text.push(`
if (this.challenge.head) { <p>Format of file:</p>
text.push(text, this.challenge.head.join('\n')); <p>
<code>
&lt!--<em>name.ext</em>--&gt<br>
&lt!--<em>contents</em>--&gt<br>
<em>Contents</em><br>
&lt!--<em>end</em>--&gt<br>
&lt!--<em>head</em>--&gt<br>
<em>Head</em><br>
&lt!--<em>end</em>--&gt<br>
&lt!--<em>tail</em>--&gt<br>
<em>Tail</em><br>
&lt!--<em>end</em>--&gt<br>
&lt!--<em>end</em>--&gt<br>
</code>
</p>`);
text.push('<div class="unpacked">');
text.push('<!--files-->');
if (this.challenge.files) {
Object.keys(this.challenge.files).forEach(key => {
let file = this.challenge.files[key];
let { contents, head, tail } = file;
text.push(`<h3>${file.name + '.' + file.ext}</h3>`);
text.push(`<!--${file.name + '.' + file.ext}-->`);
text.push('<h4>Contents</h4>');
text.push('<pre>');
text.push(_.escape(contents.join('\n')));
text.push('</pre>');
text.push('<pre style="display: none;">');
text.push('<!--contents-->');
text.push(contents.join('\n'));
text.push('<!--end-->');
text.push('</pre>');
text.push('<h4>Head</h4>');
text.push('<pre>');
text.push(_.escape(head.join('\n')));
text.push('</pre>');
text.push('<pre style="display: none;">');
text.push('<!--head-->');
text.push(head.join('\n'));
text.push('<!--end-->');
text.push('</pre>');
text.push('<h4>Tail</h4>');
text.push('<pre>');
text.push(_.escape(tail.join('\n')));
text.push('</pre>');
text.push('<pre style="display: none;">');
text.push('<!--tail-->');
text.push(tail.join('\n'));
text.push('<!--end-->');
text.push('</pre>');
text.push('<!--end-->');
});
} }
text.push('</script><!--end-->'); text.push('<!--end-->');
text.push('</div>');
text.push(''); text.push('');
text.push('<h2>Solution</h2>'); text.push('<h2>Solution</h2>');
@ -253,29 +435,24 @@ class UnpackedChallenge {
// Note: none of the challenges have more than one solution // Note: none of the challenges have more than one solution
// todo: should we deal with multiple solutions or not? // todo: should we deal with multiple solutions or not?
if (this.challenge.solutions && this.challenge.solutions.length > 0) { 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(solution);
} }
text.push('</script><!--end-->'); text.push('</script><!--end-->');
text.push('');
text.push('<h2>Tail</h2>');
text.push('<!--tail--><script class="unpacked tail">');
if (this.challenge.tail) {
text.push(text, this.challenge.tail.join('\n'));
}
text.push('</script><!--end-->');
text.push(''); text.push('');
text.push('<h2>Tests</h2>'); text.push('<h2>Tests</h2>');
text.push('<script class="unpacked tests">'); text.push('<script class="unpacked tests">');
text.push(`test(\'${this.challenge.title} challenge tests\', ` + text.push(
'function(t) {'); `test(\'${this.challenge.title} challenge tests\', ` + 'function(t) {'
);
text.push('let assert = addAssertsToTapTest(t);'); text.push('let assert = addAssertsToTapTest(t);');
text.push('let code = document.getElementById(\'solution\').innerText;'); text.push("let code = document.getElementById('solution').innerText;");
text.push('t.plan(' + text.push(
(this.challenge.tests ? this.challenge.tests.length : 0) + 't.plan(' +
');'); (this.challenge.tests ? this.challenge.tests.length : 0) +
');'
);
text.push('/*--tests--*/'); text.push('/*--tests--*/');
text.push(this.expandedTests(this.challenge.tests).join('\n')); text.push(this.expandedTests(this.challenge.tests).join('\n'));
text.push('/*--end--*/'); text.push('/*--end--*/');
@ -289,5 +466,4 @@ class UnpackedChallenge {
} }
} }
export {UnpackedChallenge}; export { UnpackedChallenge };