Skip to content

Commit 46b64db

Browse files
authored
Merge pull request #33 from coderoad/feature/validate-markdown
Feature/validate markdown
2 parents 2587efd + 183a5cd commit 46b64db

File tree

4 files changed

+219
-2
lines changed

4 files changed

+219
-2
lines changed

Diff for: package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@coderoad/cli",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"description": "A CLI to build the configuration file for Coderoad Tutorials",
55
"keywords": [
66
"coderoad",
@@ -25,7 +25,7 @@
2525
],
2626
"main": "bin/coderoad",
2727
"bin": {
28-
"@coderoad/coderoad": "bin/coderoad",
28+
"@coderoad/cli": "bin/coderoad",
2929
"coderoad": "bin/coderoad"
3030
},
3131
"scripts": {

Diff for: src/build.ts

+13
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { getCommits, CommitLogObject } from "./utils/commits";
88
import skeletonSchema from "./schema/skeleton";
99
import tutorialSchema from "./schema/tutorial";
1010
import { validateSchema } from "./utils/validateSchema";
11+
import { validateMarkdown } from "./utils/validateMarkdown";
1112
import * as T from "../typings/tutorial";
1213

1314
const write = util.promisify(fs.writeFile);
@@ -72,6 +73,18 @@ async function build(args: string[]) {
7273
return;
7374
}
7475

76+
// validate markdown loosely
77+
try {
78+
const isValid = validateMarkdown(_markdown);
79+
if (!isValid) {
80+
console.warn("Invalid markdown");
81+
}
82+
} catch (e) {
83+
console.error("Error validating markdown:");
84+
console.error(e.message);
85+
return;
86+
}
87+
7588
// parse yaml skeleton config
7689
let skeleton;
7790
try {

Diff for: src/utils/validateMarkdown.ts

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
type Validation = {
2+
message: string;
3+
validate: (t: string) => boolean;
4+
};
5+
6+
const validations: Validation[] = [
7+
{
8+
message: "should start with a title",
9+
validate: (t) => !!t.match(/^#\s.+/),
10+
},
11+
{
12+
message: "should not have multiple `#` headers",
13+
validate: (t) => !t.match(/[\n\r]#\s/),
14+
},
15+
{
16+
message: "should have a summary description under the title",
17+
validate: (t) => {
18+
const [summary] = t.split(/[\n\r]##/) || [""];
19+
const description = summary
20+
.split(/\n/)
21+
.slice(1)
22+
.filter((l) => l.length);
23+
return !!description.length;
24+
},
25+
},
26+
{
27+
message: "should have a level `##` with a format of `L[0-9]+`",
28+
validate: (t) => {
29+
const headers = t.match(/^#{2}\s(.+)$/gm) || [];
30+
for (const header of headers) {
31+
if (!header.match(/^#{2}\s(L\d+)\s(.+)$/)) {
32+
return false;
33+
}
34+
}
35+
return true;
36+
},
37+
},
38+
{
39+
message: "should have a step `###` with a format of `L[0-9]+S[0-9]+`",
40+
validate: (t) => {
41+
const headers = t.match(/^#{3}\s(.+)$/gm) || [];
42+
for (const header of headers) {
43+
if (!header.match(/^#{3}\s(L\d+)S\d+/)) {
44+
return false;
45+
}
46+
}
47+
return true;
48+
},
49+
},
50+
];
51+
52+
const codeBlockRegex = /```[a-z]*\n[\s\S]*?\n```/gm;
53+
54+
export function validateMarkdown(md: string): boolean {
55+
// remove codeblocks which might contain any valid combinations
56+
const text = md.replace(codeBlockRegex, "");
57+
58+
let valid = true;
59+
60+
for (const v of validations) {
61+
if (!v.validate(text)) {
62+
valid = false;
63+
if (process.env.NODE_ENV !== "test") {
64+
console.warn(v.message);
65+
}
66+
}
67+
}
68+
69+
return valid;
70+
}

Diff for: tests/markdown.test.ts

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { validateMarkdown } from "../src/utils/validateMarkdown";
2+
3+
describe("validate markdown", () => {
4+
it("should return false if missing a summary title (#)", () => {
5+
const md = `
6+
Description.
7+
8+
## L1 Put Level's title here
9+
10+
> Level's summary: a short description of the level's content in one line.
11+
12+
Some text that describes the level`;
13+
expect(validateMarkdown(md)).toBe(false);
14+
});
15+
16+
it("should return false if contains multiple `#` headers", () => {
17+
const md1 = `# A Title
18+
Description.
19+
20+
# Another Title
21+
22+
## L1 Put Level's title here
23+
24+
> Level's summary: a short description of the level's content in one line.
25+
26+
Some text that describes the level`;
27+
28+
const md2 = `# A Title
29+
Description.
30+
31+
32+
## L1 Put Level's title here
33+
34+
> Level's summary: a short description of the level's content in one line.
35+
36+
Some text that describes the level
37+
38+
# Another title
39+
`;
40+
41+
expect(validateMarkdown(md1)).toBe(false);
42+
expect(validateMarkdown(md2)).toBe(false);
43+
});
44+
45+
it("should return false if missing a summary description", () => {
46+
const md = `# A Title
47+
48+
## L1 Put Level's title here
49+
50+
> Level's summary: a short description of the level's content in one line.
51+
52+
Some text that describes the level
53+
`;
54+
expect(validateMarkdown(md)).toBe(false);
55+
});
56+
57+
it("should return false if `##` doesn't preface a level", () => {
58+
const md = `# A Title
59+
60+
A description
61+
62+
## Put Level's title here
63+
64+
> Level's summary: a short description of the level's content in one line.
65+
66+
Some text that describes the level
67+
`;
68+
expect(validateMarkdown(md)).toBe(false);
69+
});
70+
71+
it("should return false if `###` doesn't preface a step", () => {
72+
const md = `# A Title
73+
74+
A description
75+
76+
## Put Level's title here
77+
78+
> Level's summary: a short description of the level's content in one line.
79+
80+
Some text that describes the level
81+
82+
### A Step
83+
84+
First step
85+
`;
86+
});
87+
88+
it("should return true for valid markdown", () => {
89+
const md = `# Title
90+
91+
Description.
92+
93+
## L1 Put Level's title here
94+
95+
> Level's summary: a short description of the level's content in one line.
96+
97+
Some text that describes the level
98+
99+
### L1S1
100+
101+
First Step`;
102+
expect(validateMarkdown(md)).toBe(true);
103+
});
104+
105+
it("should ignore markdown content in codeblocks", () => {
106+
const md = `# Title
107+
108+
Description.
109+
110+
\`\`\`md
111+
# A codeblock
112+
113+
Should not be a problem
114+
\`\`\`
115+
116+
117+
## L1 Put Level's title here
118+
119+
> Level's summary: a short description of the level's content in one line.
120+
121+
Some text that describes the level
122+
123+
\`\`\`
124+
## Another Level in markdown
125+
126+
Should not be an issue
127+
\`\`\`
128+
129+
### L1S1
130+
131+
First Step`;
132+
expect(validateMarkdown(md)).toBe(true);
133+
});
134+
});

0 commit comments

Comments
 (0)