diff --git a/package.json b/package.json index 727b3fe..5ac564c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@coderoad/cli", - "version": "0.2.0", + "version": "0.2.1", "description": "A CLI to build the configuration file for Coderoad Tutorials", "keywords": [ "coderoad", @@ -25,7 +25,7 @@ ], "main": "bin/coderoad", "bin": { - "@coderoad/coderoad": "bin/coderoad", + "@coderoad/cli": "bin/coderoad", "coderoad": "bin/coderoad" }, "scripts": { diff --git a/src/build.ts b/src/build.ts index b129b52..150dbc0 100644 --- a/src/build.ts +++ b/src/build.ts @@ -8,6 +8,7 @@ import { getCommits, CommitLogObject } from "./utils/commits"; import skeletonSchema from "./schema/skeleton"; import tutorialSchema from "./schema/tutorial"; import { validateSchema } from "./utils/validateSchema"; +import { validateMarkdown } from "./utils/validateMarkdown"; import * as T from "../typings/tutorial"; const write = util.promisify(fs.writeFile); @@ -72,6 +73,18 @@ async function build(args: string[]) { return; } + // validate markdown loosely + try { + const isValid = validateMarkdown(_markdown); + if (!isValid) { + console.warn("Invalid markdown"); + } + } catch (e) { + console.error("Error validating markdown:"); + console.error(e.message); + return; + } + // parse yaml skeleton config let skeleton; try { diff --git a/src/utils/validateMarkdown.ts b/src/utils/validateMarkdown.ts new file mode 100644 index 0000000..c4d13bb --- /dev/null +++ b/src/utils/validateMarkdown.ts @@ -0,0 +1,70 @@ +type Validation = { + message: string; + validate: (t: string) => boolean; +}; + +const validations: Validation[] = [ + { + message: "should start with a title", + validate: (t) => !!t.match(/^#\s.+/), + }, + { + message: "should not have multiple `#` headers", + validate: (t) => !t.match(/[\n\r]#\s/), + }, + { + message: "should have a summary description under the title", + validate: (t) => { + const [summary] = t.split(/[\n\r]##/) || [""]; + const description = summary + .split(/\n/) + .slice(1) + .filter((l) => l.length); + return !!description.length; + }, + }, + { + message: "should have a level `##` with a format of `L[0-9]+`", + validate: (t) => { + const headers = t.match(/^#{2}\s(.+)$/gm) || []; + for (const header of headers) { + if (!header.match(/^#{2}\s(L\d+)\s(.+)$/)) { + return false; + } + } + return true; + }, + }, + { + message: "should have a step `###` with a format of `L[0-9]+S[0-9]+`", + validate: (t) => { + const headers = t.match(/^#{3}\s(.+)$/gm) || []; + for (const header of headers) { + if (!header.match(/^#{3}\s(L\d+)S\d+/)) { + return false; + } + } + return true; + }, + }, +]; + +const codeBlockRegex = /```[a-z]*\n[\s\S]*?\n```/gm; + +export function validateMarkdown(md: string): boolean { + // remove codeblocks which might contain any valid combinations + const text = md.replace(codeBlockRegex, ""); + + let valid = true; + + for (const v of validations) { + if (!v.validate(text)) { + valid = false; + if (process.env.NODE_ENV !== "test") { + console.warn(v.message); + } + } + } + + return valid; +} diff --git a/tests/markdown.test.ts b/tests/markdown.test.ts new file mode 100644 index 0000000..9b90028 --- /dev/null +++ b/tests/markdown.test.ts @@ -0,0 +1,134 @@ +import { validateMarkdown } from "../src/utils/validateMarkdown"; + +describe("validate markdown", () => { + it("should return false if missing a summary title (#)", () => { + const md = ` +Description. + +## L1 Put Level's title here + +> Level's summary: a short description of the level's content in one line. + +Some text that describes the level`; + expect(validateMarkdown(md)).toBe(false); + }); + + it("should return false if contains multiple `#` headers", () => { + const md1 = `# A Title +Description. + +# Another Title + +## L1 Put Level's title here + +> Level's summary: a short description of the level's content in one line. + +Some text that describes the level`; + + const md2 = `# A Title +Description. + + +## L1 Put Level's title here + +> Level's summary: a short description of the level's content in one line. + +Some text that describes the level + +# Another title +`; + + expect(validateMarkdown(md1)).toBe(false); + expect(validateMarkdown(md2)).toBe(false); + }); + + it("should return false if missing a summary description", () => { + const md = `# A Title + +## L1 Put Level's title here + +> Level's summary: a short description of the level's content in one line. + +Some text that describes the level +`; + expect(validateMarkdown(md)).toBe(false); + }); + + it("should return false if `##` doesn't preface a level", () => { + const md = `# A Title + +A description + +## Put Level's title here + +> Level's summary: a short description of the level's content in one line. + +Some text that describes the level +`; + expect(validateMarkdown(md)).toBe(false); + }); + + it("should return false if `###` doesn't preface a step", () => { + const md = `# A Title + +A description + +## Put Level's title here + +> Level's summary: a short description of the level's content in one line. + +Some text that describes the level + +### A Step + +First step +`; + }); + + it("should return true for valid markdown", () => { + const md = `# Title + +Description. + +## L1 Put Level's title here + +> Level's summary: a short description of the level's content in one line. + +Some text that describes the level + +### L1S1 + +First Step`; + expect(validateMarkdown(md)).toBe(true); + }); + + it("should ignore markdown content in codeblocks", () => { + const md = `# Title + +Description. + +\`\`\`md +# A codeblock + +Should not be a problem +\`\`\` + + +## L1 Put Level's title here + +> Level's summary: a short description of the level's content in one line. + +Some text that describes the level + +\`\`\` +## Another Level in markdown + +Should not be an issue +\`\`\` + +### L1S1 + +First Step`; + expect(validateMarkdown(md)).toBe(true); + }); +});