feat(seed): unpack/repack properly handles paragraph breaks

This commit is contained in:
Alex Chaffee
2018-03-23 15:53:59 -04:00
committed by Mrugesh Mohapatra
parent 590f646263
commit 14c9ed8974
4 changed files with 79 additions and 56 deletions

View File

@ -19,8 +19,11 @@ For each challenge section, there is a JSON file (fields documented below) conta
`npm run unpack` extracts challenges into separate files for easier viewing and editing. The files are `.gitignore`d and will *not* be checked in, and all mongo seed importing will keep using the existing system; this is essentially a tool for editing `challenge.json` files. These HTML files are self-contained and run their own tests -- open a browser JS console to see the test results. `npm run unpack` extracts challenges into separate files for easier viewing and editing. The files are `.gitignore`d and will *not* be checked in, and all mongo seed importing will keep using the existing system; this is essentially a tool for editing `challenge.json` files. These HTML files are self-contained and run their own tests -- open a browser JS console to see the test results.
`npm run repack` gathers up the unpacked/edited HTML files into challenge-block JSON files. Use `git diff` to see the changes `npm run repack` gathers up the unpacked/edited HTML files into challenge-block JSON files. Use `git diff` to see the changes.
When editing the unpacked files, you must only edit lines between comment fences like `<!--description-->` and `<!--end-->`. In descriptions, you can insert a paragraph break with `<!--break-->`.
Unpacked lines that begin with `//--JSON:` are parsed and inserted verbatim.
## Links ## Links

View File

@ -19,25 +19,9 @@ function directoriesIn(parentDir) {
} }
let superBlocks = directoriesIn(unpackedRoot); let superBlocks = directoriesIn(unpackedRoot);
console.log(superBlocks);
function diffFiles(originalFilePath, changedFilePath) {
// todo: async
console.log(`diffing ${originalFilePath} and ${changedFilePath}`);
let original = fs.readFileSync(originalFilePath).toString();
let repacked = fs.readFileSync(changedFilePath).toString();
let changes = jsdiff.diffLines(original, repacked, { newlineIsToken: true });
changes.forEach((change) => {
if (change.added || change.removed) {
console.log(JSON.stringify(change, null, 2));
}
});
console.log('');
}
superBlocks.forEach(superBlock => { superBlocks.forEach(superBlock => {
let superBlockPath = path.join(unpackedRoot, superBlock); let superBlockPath = path.join(unpackedRoot, superBlock);
console.log(`Repacking ${superBlockPath}...`);
let blocks = directoriesIn(superBlockPath); let blocks = directoriesIn(superBlockPath);
blocks.forEach(blockName => { blocks.forEach(blockName => {
let blockPath = path.join(superBlockPath, blockName); let blockPath = path.join(superBlockPath, blockName);
@ -59,13 +43,6 @@ superBlocks.forEach(superBlock => {
path.join(seedChallengesRoot, superBlock, blockName + '.json'); path.join(seedChallengesRoot, superBlock, blockName + '.json');
// todo: async // todo: async
fs.writeFileSync(outputFilePath, JSON.stringify(block, null, 2)); fs.writeFileSync(outputFilePath, JSON.stringify(block, null, 2));
// todo: make this a command-line option instead
let doDiff = false;
if (doDiff) {
diffFiles(blockFilePath, outputFilePath);
}
}); });
}); });

View File

@ -17,11 +17,18 @@ import {UnpackedChallenge, ChallengeFile} from './unpackedChallenge';
// bundle up the test-running JS // bundle up the test-running JS
function createUnpackedBundle() { function createUnpackedBundle() {
let unpackedDir = path.join(__dirname, 'unpacked');
fs.mkdirp(unpackedDir, (err) => {
if (err && err.code !== 'EEXIST') {
console.log(err);
throw err;
}
let unpackedFile = path.join(__dirname, 'unpacked.js'); let unpackedFile = path.join(__dirname, 'unpacked.js');
let b = browserify(unpackedFile).bundle(); let b = browserify(unpackedFile).bundle();
b.on('error', console.error); b.on('error', console.error);
let unpackedBundleFile = let unpackedBundleFile =
path.join(__dirname, 'unpacked', 'unpacked-bundle.js'); path.join(unpackedDir, 'unpacked-bundle.js');
const bundleFileStream = fs.createWriteStream(unpackedBundleFile); const bundleFileStream = fs.createWriteStream(unpackedBundleFile);
bundleFileStream.on('finish', () => { bundleFileStream.on('finish', () => {
console.log('Wrote bundled JS into ' + unpackedBundleFile); console.log('Wrote bundled JS into ' + unpackedBundleFile);
@ -32,6 +39,7 @@ function createUnpackedBundle() {
bundleFileStream.on('error', console.error); bundleFileStream.on('error', console.error);
b.pipe(bundleFileStream); b.pipe(bundleFileStream);
// bundleFileStream.end(); // do not do this prematurely! // bundleFileStream.end(); // do not do this prematurely!
});
} }
let currentlyUnpackingDir = null; let currentlyUnpackingDir = null;

View File

@ -4,6 +4,7 @@ import path from 'path';
import _ from 'lodash'; import _ from 'lodash';
const jsonLinePrefix = '//--JSON:'; const jsonLinePrefix = '//--JSON:';
const paragraphBreak = '<!--break-->';
class ChallengeFile { class ChallengeFile {
constructor(dir, name, suffix) { constructor(dir, name, suffix) {
@ -27,6 +28,7 @@ class ChallengeFile {
}); });
} }
readChunks() { readChunks() {
// todo: make this work async // todo: make this work async
// todo: make sure it works with encodings // todo: make sure it works with encodings
@ -34,17 +36,39 @@ class ChallengeFile {
let lines = data.toString().split(/(?:\r\n|\r|\n)/g); let lines = data.toString().split(/(?:\r\n|\r|\n)/g);
let chunks = {}; let chunks = {};
let readingChunk = null; let readingChunk = null;
let currentParagraph = [];
function removeLeadingEmptyLines(array) {
let emptyString = /^\s*$/;
while (array && Array.isArray(array) && emptyString.test(array[0])) {
array.shift();
}
}
lines.forEach(line => { lines.forEach(line => {
let chunkEnd = /(<!|\/\*)--end--/; let chunkEnd = /(<!|\/\*)--end--/;
let chunkStart = /(<!|\/\*)--(\w+)--/; let chunkStart = /(<!|\/\*)--(\w+)--/;
line = line.toString(); line = line.toString();
function pushParagraph() {
removeLeadingEmptyLines(currentParagraph);
chunks[ readingChunk ].push(currentParagraph.join('\n'));
currentParagraph = [];
}
if (chunkEnd.test(line)) { if (chunkEnd.test(line)) {
if (!readingChunk) { if (!readingChunk) {
throw 'Encountered --end-- without being in a chunk'; throw 'Encountered --end-- without being in a chunk';
} }
if (currentParagraph.length) {
pushParagraph();
} else {
removeLeadingEmptyLines(chunks[readingChunk]);
}
readingChunk = null; readingChunk = null;
} else if (readingChunk === 'description' && line === paragraphBreak) {
pushParagraph();
} else if (chunkStart.test(line)) { } else if (chunkStart.test(line)) {
let chunkName = line.match(chunkStart)[ 2 ]; let chunkName = line.match(chunkStart)[ 2 ];
if (readingChunk) { if (readingChunk) {
@ -54,25 +78,30 @@ class ChallengeFile {
} }
readingChunk = chunkName; readingChunk = chunkName;
} else if (readingChunk) { } else if (readingChunk) {
if (!chunks[ readingChunk ]) {
chunks[ readingChunk ] = [];
}
if (line.startsWith(jsonLinePrefix)) { if (line.startsWith(jsonLinePrefix)) {
line = JSON.parse(line.slice(jsonLinePrefix.length)); line = JSON.parse(line.slice(jsonLinePrefix.length));
} chunks[ readingChunk ].push(line);
if (!chunks[readingChunk]) { } else if (readingChunk === 'description') {
chunks[readingChunk] = []; currentParagraph.push(line);
} } else {
// don't push empty top lines
if (!(!line && chunks[readingChunk].length === 0)) {
chunks[ readingChunk ].push(line); chunks[ readingChunk ].push(line);
} }
} }
}); });
// 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 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') ];
} }
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;
} }
@ -116,15 +145,16 @@ class UnpackedChallenge {
return `${prefix}-${this.challenge.id}`; return `${prefix}-${this.challenge.id}`;
} }
expandedDescription(description) { expandedDescription() {
let out = []; let out = [];
description.forEach(part => { this.challenge.description.forEach(part => {
if (_.isString(part)) { if (_.isString(part)) {
out.push(part.toString()); out.push(part.toString());
out.push(paragraphBreak);
} else { } else {
// Descriptions are weird since sometimes they're text and sometimes // Descriptions are weird since sometimes they're text and sometimes
// they're "steps" which appear one at a time with optional pix and // they're "steps" which appear one at a time with optional pix and
// captions and links, or "questions" with choices and expanations... // captions and links, or "questions" with choices and explanations...
// For now we preserve non-string descriptions via JSON but this is // For now we preserve non-string descriptions via JSON but this is
// not a great solution. // not a great solution.
// It would be better if "steps" and "description" were separate fields. // It would be better if "steps" and "description" were separate fields.
@ -136,7 +166,10 @@ class UnpackedChallenge {
out.push(jsonLinePrefix + JSON.stringify(part)); out.push(jsonLinePrefix + JSON.stringify(part));
} }
}); });
// indent by 2
if (out[ out.length - 1 ] === paragraphBreak) {
out.pop();
}
return out; return out;
} }
@ -171,15 +204,17 @@ class UnpackedChallenge {
(challenge id <code>${this.challenge.id}</code>).</p>`); (challenge id <code>${this.challenge.id}</code>).</p>`);
text.push('<p>Open the JavaScript console to see test results.</p>'); text.push('<p>Open the JavaScript console to see test results.</p>');
// text.push(`<p>Edit this HTML file (between &lt;!--s only!) text.push(`<p>Edit this HTML file (between &lt;!-- marks only!)
// and run <code>npm 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(''); 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-->');
text.push(this.expandedDescription(this.challenge.description).join('\n')); if (this.challenge.description.length) {
text.push(this.expandedDescription().join('\n'));
}
text.push('<!--end-->'); text.push('<!--end-->');
text.push('</div>'); text.push('</div>');
@ -218,7 +253,7 @@ 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-->');