feat(seed): unpack/repack properly handles paragraph breaks
This commit is contained in:
committed by
Mrugesh Mohapatra
parent
590f646263
commit
14c9ed8974
@ -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
|
||||||
|
|
||||||
|
25
repack.js
25
repack.js
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
36
unpack.js
36
unpack.js
@ -17,21 +17,29 @@ import {UnpackedChallenge, ChallengeFile} from './unpackedChallenge';
|
|||||||
|
|
||||||
// bundle up the test-running JS
|
// bundle up the test-running JS
|
||||||
function createUnpackedBundle() {
|
function createUnpackedBundle() {
|
||||||
let unpackedFile = path.join(__dirname, 'unpacked.js');
|
let unpackedDir = path.join(__dirname, 'unpacked');
|
||||||
let b = browserify(unpackedFile).bundle();
|
fs.mkdirp(unpackedDir, (err) => {
|
||||||
b.on('error', console.error);
|
if (err && err.code !== 'EEXIST') {
|
||||||
let unpackedBundleFile =
|
console.log(err);
|
||||||
path.join(__dirname, 'unpacked', 'unpacked-bundle.js');
|
throw err;
|
||||||
const bundleFileStream = fs.createWriteStream(unpackedBundleFile);
|
}
|
||||||
bundleFileStream.on('finish', () => {
|
|
||||||
console.log('Wrote bundled JS into ' + unpackedBundleFile);
|
let unpackedFile = path.join(__dirname, 'unpacked.js');
|
||||||
|
let b = browserify(unpackedFile).bundle();
|
||||||
|
b.on('error', console.error);
|
||||||
|
let unpackedBundleFile =
|
||||||
|
path.join(unpackedDir, 'unpacked-bundle.js');
|
||||||
|
const bundleFileStream = fs.createWriteStream(unpackedBundleFile);
|
||||||
|
bundleFileStream.on('finish', () => {
|
||||||
|
console.log('Wrote bundled JS into ' + unpackedBundleFile);
|
||||||
|
});
|
||||||
|
bundleFileStream.on('pipe', () => {
|
||||||
|
console.log('Writing bundled JS...');
|
||||||
|
});
|
||||||
|
bundleFileStream.on('error', console.error);
|
||||||
|
b.pipe(bundleFileStream);
|
||||||
|
// bundleFileStream.end(); // do not do this prematurely!
|
||||||
});
|
});
|
||||||
bundleFileStream.on('pipe', () => {
|
|
||||||
console.log('Writing bundled JS...');
|
|
||||||
});
|
|
||||||
bundleFileStream.on('error', console.error);
|
|
||||||
b.pipe(bundleFileStream);
|
|
||||||
// bundleFileStream.end(); // do not do this prematurely!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentlyUnpackingDir = null;
|
let currentlyUnpackingDir = null;
|
||||||
|
@ -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 <!--s only!)
|
text.push(`<p>Edit this HTML file (between <!-- 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-->');
|
||||||
|
Reference in New Issue
Block a user