Skip to content

Commit 9c0b8c3

Browse files
authored
Merge pull request #4 from coderoad/feature/tests
Feature/tests
2 parents e27add1 + c1f89ee commit 9c0b8c3

File tree

6 files changed

+196
-121
lines changed

6 files changed

+196
-121
lines changed

package.json

+8-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@
4343
},
4444
"jest": {
4545
"preset": "ts-jest",
46-
"testEnvironment": "node"
46+
"verbose": true,
47+
"testPathIgnorePatterns": [
48+
"build"
49+
],
50+
"moduleFileExtensions": [
51+
"js",
52+
"ts"
53+
]
4754
}
4855
}

src/build.ts

+4-96
Original file line numberDiff line numberDiff line change
@@ -3,84 +3,14 @@ import * as path from "path";
33
import * as _ from "lodash";
44
import * as fs from "fs";
55
import * as T from "../typings/tutorial";
6+
import { parse } from "./utils/parse";
67
// import validate from './validator';
78

89
// import not working
910
const simpleGit = require("simple-git/promise");
1011

1112
const workingDir = "tmp";
1213

13-
type TutorialContent = {};
14-
15-
function parseContent(md: string): TutorialContent {
16-
let start: number = -1;
17-
const parts: any[] = [];
18-
19-
const lines = md.split("\n");
20-
21-
// Split the multiple parts - This way enables the creator to use 4/5 level headers inside the content.
22-
lines.forEach((line, index) => {
23-
if (line.match(/#{1,3}\s/) || index === lines.length - 1) {
24-
if (start !== -1) {
25-
parts.push(lines.slice(start, index).join("\n"));
26-
start = index;
27-
} else {
28-
start = index;
29-
}
30-
}
31-
});
32-
33-
const sections = {};
34-
35-
// Identify and remove the header
36-
const summaryMatch = parts
37-
.shift()
38-
.match(/^#\s(?<tutorialTitle>.*)[\n\r]+(?<tutorialDescription>[^]*)/);
39-
40-
sections["summary"] = {
41-
title: summaryMatch.groups.tutorialTitle.trim(),
42-
description: summaryMatch.groups.tutorialDescription.trim(),
43-
};
44-
45-
// Identify each part of the content
46-
parts.forEach((section) => {
47-
const levelRegex = /^(##\s(?<levelId>L\d+)\s(?<levelTitle>.*)[\n\r]*(>\s*(?<levelSummary>.*))?[\n\r]+(?<levelContent>[^]*))/;
48-
const stepRegex = /^(###\s(?<stepId>(?<levelId>L\d+)S\d+)\s(?<stepTitle>.*)[\n\r]+(?<stepContent>[^]*))/;
49-
50-
const levelMatch = section.match(levelRegex);
51-
const stepMatch = section.match(stepRegex);
52-
53-
if (levelMatch) {
54-
const level = {
55-
[levelMatch.groups.levelId]: {
56-
id: levelMatch.groups.levelId,
57-
title: levelMatch.groups.levelTitle,
58-
summary: levelMatch.groups.levelSummary.trim(),
59-
content: levelMatch.groups.levelContent.trim(),
60-
},
61-
};
62-
63-
_.merge(sections, level);
64-
} else if (stepMatch) {
65-
const step = {
66-
[stepMatch.groups.levelId]: {
67-
steps: {
68-
[stepMatch.groups.stepId]: {
69-
id: stepMatch.groups.stepId,
70-
// title: stepMatch.groups.stepTitle, //Not using at this momemnt
71-
content: stepMatch.groups.stepContent.trim(),
72-
},
73-
},
74-
},
75-
};
76-
77-
_.merge(sections, step);
78-
}
79-
});
80-
81-
return sections;
82-
}
83-
8414
function rmDir(dir: string, rmSelf = false) {
8515
try {
8616
let files;
@@ -164,32 +94,10 @@ async function build({ repo, codeBranch, setupBranch, isLocal }: BuildOptions) {
16494
await git.checkout(setupBranch);
16595

16696
// Load files
167-
const _mdContent = fs.readFileSync(
168-
path.join(localPath, "TUTORIAL.md"),
169-
"utf8"
170-
);
171-
let _tutorial = fs.readFileSync(
172-
path.join(localPath, "coderoad.yaml"),
173-
"utf8"
174-
);
175-
176-
// Add one more line to the content as per Shawn's request
177-
const mdContent: any = parseContent(_mdContent);
178-
179-
// Parse tutorial to JSON
180-
const tutorial: T.Tutorial = yamlParser.load(_tutorial);
181-
182-
// Add the summary to the tutorial file
183-
tutorial.summary = mdContent.summary;
184-
185-
// merge content and tutorial
186-
tutorial.levels.forEach((level: T.Level) => {
187-
const { steps, ...content } = mdContent[level.id];
188-
189-
level.steps.forEach((step: T.Step) => _.merge(step, steps[step.id]));
97+
const _content = fs.readFileSync(path.join(localPath, "TUTORIAL.md"), "utf8");
98+
let _config = fs.readFileSync(path.join(localPath, "coderoad.yaml"), "utf8");
19099

191-
_.merge(level, content);
192-
});
100+
const tutorial = parse(_content, _config);
193101

194102
// Checkout the code branches
195103
await git.checkout(codeBranch);

src/templates/coderoad.yaml

+17-23
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,7 @@
22
# This is a YAML-formatted file.
33
## Your personal version of the tutorial
44
##
5-
version: '0.1.0'
6-
## Data used to populate the tutorial summary page
7-
##
8-
summary:
9-
## The title of your tutorial. Required.
10-
##
11-
title: ''
12-
## A description of your tutorial. Required.
13-
##
14-
description: ''
5+
version: "0.1.0"
156
## Data used to configure and setup the tutorial
167
##
178
config:
@@ -32,28 +23,31 @@ config:
3223
setup:
3324
## A list of commits to load to setup the tutorial
3425
commits: []
35-
# - commit1
36-
# - commit2
26+
# - commit1
27+
# - commit2
3728
## A list of commands to run to configure the tutorial
38-
commands: []
29+
commands:
30+
[]
3931
# - npm install
4032
## App versions helps to ensure compatability with the Extension
41-
appVersions: {}
33+
appVersions:
34+
{}
4235
## Ensure compatability with a minimal VSCode CodeRoad version
4336
# vscode: '>=0.7.0'
4437
## Repo information to load code from
4538
##
4639
repo:
4740
## The uri path to the repo containing the code commits. Required.
4841
##
49-
uri: ''
42+
uri: ""
5043
## The branch on the repo uri that contains the code commits. Required.
51-
branch: ''
52-
44+
branch: ""
45+
5346
## A list of tutorial dependencies to ensure the environment is setup for the tutorial. Optional.
5447
## The dependencies will be checked by running `dependency.name` --version and comparing it to the version provided.
5548
##
56-
dependencies: []
49+
dependencies:
50+
[]
5751
## The name of the dependency
5852
# - name: node
5953
# ## The version requirement. See https://github.com/npm/node-semver for options.
@@ -69,16 +63,17 @@ levels:
6963
setup:
7064
## Files to open in a text editor when the task loads. Optional.
7165
files: []
72-
# - package.json
66+
# - package.json
7367
## Commits to load when the task loads. These should include failing tests. Required.
7468
## The list will be filled by the parser
75-
commits: []
69+
commits:
70+
[]
7671
# - a commit hash
7772
## Solution for the first task. Required.
7873
solution:
7974
## Files to open when the solution loads. Optional.
8075
files: []
81-
# - package.json
76+
# - package.json
8277
## Commits that complete the task. All tests should pass when the commits load. These commits will not be loaded by the tutorial user in normal tutorial activity.
8378
## The list will be filled by the parser
8479
commits: []
@@ -117,10 +112,9 @@ levels:
117112
- commit8
118113
commands:
119114
## A filter is a regex that limits the test results
120-
- filter: '^Example 2'
115+
- filter: "^Example 2"
121116
## A feature that shows subtasks: all filtered active test names and the status of the tests (pass/fail).
122117
- subtasks: true
123118
solution:
124119
commits:
125120
- commit9
126-

src/utils/parse.ts

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import * as yamlParser from "js-yaml";
2+
import * as _ from "lodash";
3+
import * as T from "../../typings/tutorial";
4+
5+
type TutorialFrame = {
6+
summary: T.TutorialSummary;
7+
};
8+
9+
export function parseMdContent(md: string): TutorialFrame | never {
10+
let start: number = -1;
11+
const parts: any[] = [];
12+
13+
const lines = md.split("\n");
14+
15+
// Split the multiple parts - This way enables the creator to use 4/5 level headers inside the content.
16+
lines.forEach((line, index) => {
17+
if (line.match(/#{1,3}\s/) || index === lines.length - 1) {
18+
if (start !== -1) {
19+
parts.push(lines.slice(start, index).join("\n"));
20+
start = index;
21+
} else {
22+
start = index;
23+
}
24+
}
25+
});
26+
27+
const sections = {};
28+
29+
// Identify and remove the header
30+
const summaryMatch = parts
31+
.shift()
32+
.match(/^#\s(?<tutorialTitle>.*)[\n\r]+(?<tutorialDescription>[^]*)/);
33+
34+
if (!summaryMatch.groups.tutorialTitle) {
35+
throw new Error("Missing tutorial title");
36+
}
37+
38+
if (!summaryMatch.groups.tutorialDescription) {
39+
throw new Error("Missing tutorial summary description");
40+
}
41+
42+
sections["summary"] = {
43+
title: summaryMatch.groups.tutorialTitle.trim(),
44+
description: summaryMatch.groups.tutorialDescription.trim(),
45+
};
46+
47+
// Identify each part of the content
48+
parts.forEach((section) => {
49+
const levelRegex = /^(##\s(?<levelId>L\d+)\s(?<levelTitle>.*)[\n\r]*(>\s*(?<levelSummary>.*))?[\n\r]+(?<levelContent>[^]*))/;
50+
const stepRegex = /^(###\s(?<stepId>(?<levelId>L\d+)S\d+)\s(?<stepTitle>.*)[\n\r]+(?<stepContent>[^]*))/;
51+
52+
const levelMatch = section.match(levelRegex);
53+
const stepMatch = section.match(stepRegex);
54+
55+
if (levelMatch) {
56+
const level = {
57+
[levelMatch.groups.levelId]: {
58+
id: levelMatch.groups.levelId,
59+
title: levelMatch.groups.levelTitle,
60+
summary: levelMatch.groups.levelSummary.trim(),
61+
content: levelMatch.groups.levelContent.trim(),
62+
},
63+
};
64+
65+
_.merge(sections, level);
66+
} else if (stepMatch) {
67+
const step = {
68+
[stepMatch.groups.levelId]: {
69+
steps: {
70+
[stepMatch.groups.stepId]: {
71+
id: stepMatch.groups.stepId,
72+
// title: stepMatch.groups.stepTitle, //Not using at this momemnt
73+
content: stepMatch.groups.stepContent.trim(),
74+
},
75+
},
76+
},
77+
};
78+
79+
_.merge(sections, step);
80+
}
81+
});
82+
83+
// @ts-ignore
84+
return sections;
85+
}
86+
87+
export function parse(_content: string, _config: string): T.Tutorial {
88+
const mdContent: TutorialFrame = parseMdContent(_content);
89+
// Parse tutorial to JSON
90+
const tutorial: T.Tutorial = yamlParser.load(_config);
91+
92+
// Add the summary to the tutorial file
93+
tutorial["summary"] = mdContent.summary;
94+
95+
// merge content and tutorial
96+
if (tutorial.levels) {
97+
tutorial.levels.forEach((level: T.Level) => {
98+
const levelContent = mdContent[level.id];
99+
if (!levelContent) {
100+
console.log(`Markdown content not found for ${level.id}`);
101+
return;
102+
}
103+
const { steps, ...content } = levelContent;
104+
105+
if (steps) {
106+
steps.forEach((step: T.Step) => _.merge(step, steps[step.id]));
107+
}
108+
109+
_.merge(level, content);
110+
});
111+
}
112+
113+
return tutorial;
114+
}

tests/parse.test.ts

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { parse } from "../src/utils/parse";
2+
3+
describe("parse", () => {
4+
it("should parse summary", () => {
5+
const md = `# Insert Tutorial's Title here
6+
7+
Short description to be shown as a tutorial's subtitle.
8+
9+
`;
10+
11+
const yaml = `version: "0.1.0"`;
12+
const result = parse(md, yaml);
13+
const expected = {
14+
summary: {
15+
description: "Short description to be shown as a tutorial's subtitle.",
16+
title: "Insert Tutorial's Title here",
17+
},
18+
};
19+
expect(result.summary).toEqual(expected.summary);
20+
});
21+
22+
it("should parse a level with a summary", () => {
23+
const md = `# Title
24+
25+
Description.
26+
27+
## L1 Put Level's title here
28+
29+
> Level's summary: a short description of the level's content in one line.
30+
31+
Some text
32+
`;
33+
34+
const yaml = `version: "0.1.0"
35+
levels:
36+
- id: L1
37+
`;
38+
const result = parse(md, yaml);
39+
const expected = {
40+
levels: [
41+
{
42+
id: "L1",
43+
title: "Put Level's title here",
44+
summary:
45+
"Level's summary: a short description of the level's content in one line.",
46+
content: "Some text",
47+
},
48+
],
49+
};
50+
expect(result.levels).toEqual(expected.levels);
51+
});
52+
});

0 commit comments

Comments
 (0)