Skip to content

Commit 14c9ed8

Browse files
alexchraisedadead
authored andcommitted
feat(seed): unpack/repack properly handles paragraph breaks
1 parent 590f646 commit 14c9ed8

File tree

4 files changed

+79
-56
lines changed

4 files changed

+79
-56
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ For each challenge section, there is a JSON file (fields documented below) conta
1919

2020
`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.
2121

22-
`npm run repack` gathers up the unpacked/edited HTML files into challenge-block JSON files. Use `git diff` to see the changes
22+
`npm run repack` gathers up the unpacked/edited HTML files into challenge-block JSON files. Use `git diff` to see the changes.
2323

24+
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-->`.
25+
26+
Unpacked lines that begin with `//--JSON:` are parsed and inserted verbatim.
2427

2528
## Links
2629

repack.js

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,9 @@ function directoriesIn(parentDir) {
1919
}
2020

2121
let superBlocks = directoriesIn(unpackedRoot);
22-
console.log(superBlocks);
23-
24-
function diffFiles(originalFilePath, changedFilePath) {
25-
// todo: async
26-
console.log(`diffing ${originalFilePath} and ${changedFilePath}`);
27-
let original = fs.readFileSync(originalFilePath).toString();
28-
let repacked = fs.readFileSync(changedFilePath).toString();
29-
30-
let changes = jsdiff.diffLines(original, repacked, { newlineIsToken: true });
31-
changes.forEach((change) => {
32-
if (change.added || change.removed) {
33-
console.log(JSON.stringify(change, null, 2));
34-
}
35-
});
36-
console.log('');
37-
}
38-
3922
superBlocks.forEach(superBlock => {
4023
let superBlockPath = path.join(unpackedRoot, superBlock);
24+
console.log(`Repacking ${superBlockPath}...`);
4125
let blocks = directoriesIn(superBlockPath);
4226
blocks.forEach(blockName => {
4327
let blockPath = path.join(superBlockPath, blockName);
@@ -59,13 +43,6 @@ superBlocks.forEach(superBlock => {
5943
path.join(seedChallengesRoot, superBlock, blockName + '.json');
6044
// todo: async
6145
fs.writeFileSync(outputFilePath, JSON.stringify(block, null, 2));
62-
63-
// todo: make this a command-line option instead
64-
let doDiff = false;
65-
if (doDiff) {
66-
diffFiles(blockFilePath, outputFilePath);
67-
}
68-
6946
});
7047

7148
});

unpack.js

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,29 @@ import {UnpackedChallenge, ChallengeFile} from './unpackedChallenge';
1717

1818
// bundle up the test-running JS
1919
function createUnpackedBundle() {
20-
let unpackedFile = path.join(__dirname, 'unpacked.js');
21-
let b = browserify(unpackedFile).bundle();
22-
b.on('error', console.error);
23-
let unpackedBundleFile =
24-
path.join(__dirname, 'unpacked', 'unpacked-bundle.js');
25-
const bundleFileStream = fs.createWriteStream(unpackedBundleFile);
26-
bundleFileStream.on('finish', () => {
27-
console.log('Wrote bundled JS into ' + unpackedBundleFile);
28-
});
29-
bundleFileStream.on('pipe', () => {
30-
console.log('Writing bundled JS...');
20+
let unpackedDir = path.join(__dirname, 'unpacked');
21+
fs.mkdirp(unpackedDir, (err) => {
22+
if (err && err.code !== 'EEXIST') {
23+
console.log(err);
24+
throw err;
25+
}
26+
27+
let unpackedFile = path.join(__dirname, 'unpacked.js');
28+
let b = browserify(unpackedFile).bundle();
29+
b.on('error', console.error);
30+
let unpackedBundleFile =
31+
path.join(unpackedDir, 'unpacked-bundle.js');
32+
const bundleFileStream = fs.createWriteStream(unpackedBundleFile);
33+
bundleFileStream.on('finish', () => {
34+
console.log('Wrote bundled JS into ' + unpackedBundleFile);
35+
});
36+
bundleFileStream.on('pipe', () => {
37+
console.log('Writing bundled JS...');
38+
});
39+
bundleFileStream.on('error', console.error);
40+
b.pipe(bundleFileStream);
41+
// bundleFileStream.end(); // do not do this prematurely!
3142
});
32-
bundleFileStream.on('error', console.error);
33-
b.pipe(bundleFileStream);
34-
// bundleFileStream.end(); // do not do this prematurely!
3543
}
3644

3745
let currentlyUnpackingDir = null;

unpackedChallenge.js

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import path from 'path';
44
import _ from 'lodash';
55

66
const jsonLinePrefix = '//--JSON:';
7+
const paragraphBreak = '<!--break-->';
78

89
class ChallengeFile {
910
constructor(dir, name, suffix) {
@@ -27,24 +28,47 @@ class ChallengeFile {
2728
});
2829
}
2930

31+
3032
readChunks() {
3133
// todo: make this work async
3234
// todo: make sure it works with encodings
3335
let data = fs.readFileSync(this.filePath());
3436
let lines = data.toString().split(/(?:\r\n|\r|\n)/g);
3537
let chunks = {};
3638
let readingChunk = null;
39+
let currentParagraph = [];
40+
41+
function removeLeadingEmptyLines(array) {
42+
let emptyString = /^\s*$/;
43+
while (array && Array.isArray(array) && emptyString.test(array[0])) {
44+
array.shift();
45+
}
46+
}
47+
3748
lines.forEach(line => {
3849
let chunkEnd = /(<!|\/\*)--end--/;
3950
let chunkStart = /(<!|\/\*)--(\w+)--/;
4051

4152
line = line.toString();
4253

54+
function pushParagraph() {
55+
removeLeadingEmptyLines(currentParagraph);
56+
chunks[ readingChunk ].push(currentParagraph.join('\n'));
57+
currentParagraph = [];
58+
}
59+
4360
if (chunkEnd.test(line)) {
4461
if (!readingChunk) {
4562
throw 'Encountered --end-- without being in a chunk';
4663
}
64+
if (currentParagraph.length) {
65+
pushParagraph();
66+
} else {
67+
removeLeadingEmptyLines(chunks[readingChunk]);
68+
}
4769
readingChunk = null;
70+
} else if (readingChunk === 'description' && line === paragraphBreak) {
71+
pushParagraph();
4872
} else if (chunkStart.test(line)) {
4973
let chunkName = line.match(chunkStart)[ 2 ];
5074
if (readingChunk) {
@@ -54,25 +78,30 @@ class ChallengeFile {
5478
}
5579
readingChunk = chunkName;
5680
} else if (readingChunk) {
81+
if (!chunks[ readingChunk ]) {
82+
chunks[ readingChunk ] = [];
83+
}
5784
if (line.startsWith(jsonLinePrefix)) {
5885
line = JSON.parse(line.slice(jsonLinePrefix.length));
59-
}
60-
if (!chunks[readingChunk]) {
61-
chunks[readingChunk] = [];
62-
}
63-
// don't push empty top lines
64-
if (!(!line && chunks[readingChunk].length === 0)) {
86+
chunks[ readingChunk ].push(line);
87+
} else if (readingChunk === 'description') {
88+
currentParagraph.push(line);
89+
} else {
6590
chunks[ readingChunk ].push(line);
6691
}
6792
}
6893
});
6994

7095
// hack to deal with solutions field being an array of a single string
71-
// instead of an array of lines like other fields
96+
// instead of an array of lines like some other fields
7297
if (chunks.solutions) {
73-
chunks.solutions = [chunks.solutions.join('\n')];
98+
chunks.solutions = [ chunks.solutions.join('\n') ];
7499
}
75100

101+
Object.keys(chunks).forEach(key => {
102+
removeLeadingEmptyLines(chunks[key]);
103+
});
104+
76105
// console.log(JSON.stringify(chunks, null, 2));
77106
return chunks;
78107
}
@@ -116,15 +145,16 @@ class UnpackedChallenge {
116145
return `${prefix}-${this.challenge.id}`;
117146
}
118147

119-
expandedDescription(description) {
148+
expandedDescription() {
120149
let out = [];
121-
description.forEach(part => {
150+
this.challenge.description.forEach(part => {
122151
if (_.isString(part)) {
123152
out.push(part.toString());
153+
out.push(paragraphBreak);
124154
} else {
125155
// Descriptions are weird since sometimes they're text and sometimes
126156
// they're "steps" which appear one at a time with optional pix and
127-
// captions and links, or "questions" with choices and expanations...
157+
// captions and links, or "questions" with choices and explanations...
128158
// For now we preserve non-string descriptions via JSON but this is
129159
// not a great solution.
130160
// It would be better if "steps" and "description" were separate fields.
@@ -136,7 +166,10 @@ class UnpackedChallenge {
136166
out.push(jsonLinePrefix + JSON.stringify(part));
137167
}
138168
});
139-
// indent by 2
169+
170+
if (out[ out.length - 1 ] === paragraphBreak) {
171+
out.pop();
172+
}
140173
return out;
141174
}
142175

@@ -171,15 +204,17 @@ class UnpackedChallenge {
171204
(challenge id <code>${this.challenge.id}</code>).</p>`);
172205
text.push('<p>Open the JavaScript console to see test results.</p>');
173206

174-
// text.push(`<p>Edit this HTML file (between &lt;!--s only!)
175-
// and run <code>npm repack ???</code>
176-
// to incorporate your changes into the challenge database.</p>`);
207+
text.push(`<p>Edit this HTML file (between &lt;!-- marks only!)
208+
and run <code>npm run repack</code>
209+
to incorporate your changes into the challenge database.</p>`);
177210

178211
text.push('');
179212
text.push('<h2>Description</h2>');
180213
text.push('<div class="unpacked description">');
181214
text.push('<!--description-->');
182-
text.push(this.expandedDescription(this.challenge.description).join('\n'));
215+
if (this.challenge.description.length) {
216+
text.push(this.expandedDescription().join('\n'));
217+
}
183218
text.push('<!--end-->');
184219
text.push('</div>');
185220

@@ -218,7 +253,7 @@ class UnpackedChallenge {
218253
// Note: none of the challenges have more than one solution
219254
// todo: should we deal with multiple solutions or not?
220255
if (this.challenge.solutions && this.challenge.solutions.length > 0) {
221-
let solution = this.challenge.solutions[0];
256+
let solution = this.challenge.solutions[ 0 ];
222257
text.push(solution);
223258
}
224259
text.push('</script><!--end-->');

0 commit comments

Comments
 (0)