diff --git a/README.md b/README.md index ab6f445..482e61c 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,12 @@ This level has two steps... The first step with id L1S1. The Step id should start with the level id. +#### HINTS + +- The first hint available when a user requests a hint +- The second hint that will show +- The third and final hint, as it is last in order + ### L1S2 The second step The second step... diff --git a/src/schema/tutorial.ts b/src/schema/tutorial.ts index ed3cc15..6a605c9 100644 --- a/src/schema/tutorial.ts +++ b/src/schema/tutorial.ts @@ -201,6 +201,16 @@ export default { ], }, }, + hints: { + type: "array", + description: + "An optional array of hints to provide helpful feedback to users", + items: { + type: "string", + description: "A hint to provide to the user", + examples: ["Have you tried doing X?"], + }, + }, required: ["content", "setup", "solution"], }, }, diff --git a/src/templates/TUTORIAL.md b/src/templates/TUTORIAL.md index f9b3d04..c84cf7e 100644 --- a/src/templates/TUTORIAL.md +++ b/src/templates/TUTORIAL.md @@ -36,3 +36,9 @@ Short description of the step's purpose. Should be short and fit in one line ### L1S2 Another step Step's short description. + +#### Hints + +- If a hint exists, the user can request a hint +- Hints will show up in the order they are written +- When all hints are added, the hint option will become disabled diff --git a/src/utils/parse.ts b/src/utils/parse.ts index a6db41f..a97f467 100644 --- a/src/utils/parse.ts +++ b/src/utils/parse.ts @@ -49,10 +49,11 @@ export function parseMdContent(md: string): TutorialFrame | never { mdContent.summary.description = summaryMatch.groups.tutorialDescription.trim(); } + let current = { level: "0", step: "0" }; // Identify each part of the content parts.forEach((section: string) => { // match level - const levelRegex = /^(##\s(?L\d+)\s(?.*)[\n\r]*(>\s*(?.*))?[\n\r]+(?[^]*))/; + const levelRegex = /^(#{2}\s(?L\d+)\s(?.*)[\n\r]*(>\s*(?.*))?[\n\r]+(?[^]*))/; const levelMatch: RegExpMatchArray | null = section.match(levelRegex); if (levelMatch && levelMatch.groups) { const { @@ -71,16 +72,33 @@ export function parseMdContent(md: string): TutorialFrame | never { : truncate(levelContent.trim(), { length: 80, omission: "..." }), content: levelContent.trim(), }; + current = { level: levelId, step: "0" }; } else { // match step - const stepRegex = /^(###\s(?(?L\d+)S\d+)\s(?.*)[\n\r]+(?[^]*))/; + const stepRegex = /^(#{3}\s(?(?L\d+)S\d+)\s(?.*)[\n\r]+(?[^]*))/; const stepMatch: RegExpMatchArray | null = section.match(stepRegex); if (stepMatch && stepMatch.groups) { const { stepId, stepContent } = stepMatch.groups; + mdContent.steps[stepId] = { id: stepId, content: stepContent.trim(), }; + current = { ...current, step: stepId }; + } else { + // parse hints from stepContent + const hintDetectRegex = /^(#{4}\sHINTS[\n\r]+(\*\s(?[^]*))[\n\r]+)+/; + const hintMatch = section.match(hintDetectRegex); + if (!!hintMatch) { + const hintItemRegex = /[\n\r]+\*\s/; + const hints = section + .split(hintItemRegex) + .slice(1) // remove #### HINTS + .map((h) => h.trim()); + if (hints.length) { + mdContent.steps[current.step].hints = hints; + } + } } } }); @@ -135,39 +153,45 @@ export function parse(params: ParseParams): any { } // add level step commits - level.steps = (level.steps || []).map( - (step: T.Step, stepIndex: number) => { - const stepKey = `${levelSetupKey}S${stepIndex + 1}`; - const stepSetupKey = `${stepKey}Q`; - if (params.commits[stepSetupKey]) { - if (!step.setup) { - step.setup = { - commits: [], - }; + try { + level.steps = (level.steps || []).map( + (step: T.Step, stepIndex: number) => { + const stepKey = `${levelSetupKey}S${stepIndex + 1}`; + const stepSetupKey = `${stepKey}Q`; + if (params.commits[stepSetupKey]) { + if (!step.setup) { + step.setup = { + commits: [], + }; + } + step.setup.commits = params.commits[stepSetupKey]; } - step.setup.commits = params.commits[stepSetupKey]; - } - const stepSolutionKey = `${stepKey}A`; - if (params.commits[stepSolutionKey]) { - if (!step.solution) { - step.solution = { - commits: [], - }; + const stepSolutionKey = `${stepKey}A`; + if (params.commits[stepSolutionKey]) { + if (!step.solution) { + step.solution = { + commits: [], + }; + } + step.solution.commits = params.commits[stepSolutionKey]; } - step.solution.commits = params.commits[stepSolutionKey]; - } - // add markdown - const stepMarkdown: Partial = mdContent.steps[step.id]; - if (stepMarkdown) { - step = { ...step, ...stepMarkdown }; - } + // add markdown + const stepMarkdown: Partial = mdContent.steps[step.id]; + if (stepMarkdown) { + step = { ...step, ...stepMarkdown }; + } - step.id = `${stepKey}`; - return step; - } - ); + step.id = `${stepKey}`; + return step; + } + ); + } catch (error) { + console.log(JSON.stringify(level.steps)); + console.error("Error parsing level steps"); + console.error(error.message); + } return level; }) diff --git a/tests/parse.test.ts b/tests/parse.test.ts index 767b70c..7aa1722 100644 --- a/tests/parse.test.ts +++ b/tests/parse.test.ts @@ -1,32 +1,34 @@ import { parse } from "../src/utils/parse"; describe("parse", () => { - // summary - it("should parse summary", () => { - const md = `# Insert Tutorial's Title here + describe("summary", () => { + it("should parse summary", () => { + const md = `# Insert Tutorial's Title here Short description to be shown as a tutorial's subtitle. `; - const skeleton = { version: "0.1.0" }; - const result = parse({ - text: md, - skeleton, - commits: {}, + const skeleton = { version: "0.1.0" }; + const result = parse({ + text: md, + skeleton, + commits: {}, + }); + const expected = { + summary: { + description: + "Short description to be shown as a tutorial's subtitle.", + title: "Insert Tutorial's Title here", + }, + }; + expect(result.summary).toEqual(expected.summary); }); - const expected = { - summary: { - description: "Short description to be shown as a tutorial's subtitle.", - title: "Insert Tutorial's Title here", - }, - }; - expect(result.summary).toEqual(expected.summary); }); - // levels - it("should parse a level with no steps", () => { - const md = `# Title + describe("levels", () => { + it("should parse a level with no steps", () => { + const md = `# Title Description. @@ -37,32 +39,32 @@ Description. Some text `; - const skeleton = { - levels: [{ id: "L1" }], - }; - - const result = parse({ - text: md, - skeleton, - commits: {}, + const skeleton = { + levels: [{ id: "L1" }], + }; + + const result = parse({ + text: md, + skeleton, + commits: {}, + }); + const expected = { + levels: [ + { + id: "L1", + title: "Put Level's title here", + summary: + "Level's summary: a short description of the level's content in one line.", + content: "Some text", + steps: [], + }, + ], + }; + expect(result.levels).toEqual(expected.levels); }); - const expected = { - levels: [ - { - id: "L1", - title: "Put Level's title here", - summary: - "Level's summary: a short description of the level's content in one line.", - content: "Some text", - steps: [], - }, - ], - }; - expect(result.levels).toEqual(expected.levels); - }); - it("should parse a level with a step", () => { - const md = `# Title + it("should parse a level with a step", () => { + const md = `# Title Description. @@ -73,40 +75,40 @@ Description. Some text `; - const skeleton = { - levels: [ - { - id: "L1", - setup: { files: [], commits: [] }, - solution: { files: [], commits: [] }, - steps: [], - }, - ], - }; - const result = parse({ - text: md, - skeleton, - commits: {}, + const skeleton = { + levels: [ + { + id: "L1", + setup: { files: [], commits: [] }, + solution: { files: [], commits: [] }, + steps: [], + }, + ], + }; + const result = parse({ + text: md, + skeleton, + commits: {}, + }); + const expected = { + levels: [ + { + id: "L1", + title: "Put Level's title here", + summary: + "Level's summary: a short description of the level's content in one line.", + content: "Some text", + setup: { files: [], commits: [] }, + solution: { files: [], commits: [] }, + steps: [], + }, + ], + }; + expect(result.levels).toEqual(expected.levels); }); - const expected = { - levels: [ - { - id: "L1", - title: "Put Level's title here", - summary: - "Level's summary: a short description of the level's content in one line.", - content: "Some text", - setup: { files: [], commits: [] }, - solution: { files: [], commits: [] }, - steps: [], - }, - ], - }; - expect(result.levels).toEqual(expected.levels); - }); - it("should parse a level with no level description", () => { - const md = `# Title + it("should parse a level with no level description", () => { + const md = `# Title Description. @@ -115,28 +117,28 @@ Description. Some text that becomes the summary `; - const skeleton = { levels: [{ id: "L1" }] }; - const result = parse({ - text: md, - skeleton, - commits: {}, + const skeleton = { levels: [{ id: "L1" }] }; + const result = parse({ + text: md, + skeleton, + commits: {}, + }); + const expected = { + levels: [ + { + id: "L1", + title: "Put Level's title here", + summary: "Some text that becomes the summary", + content: "Some text that becomes the summary", + steps: [], + }, + ], + }; + expect(result.levels).toEqual(expected.levels); }); - const expected = { - levels: [ - { - id: "L1", - title: "Put Level's title here", - summary: "Some text that becomes the summary", - content: "Some text that becomes the summary", - steps: [], - }, - ], - }; - expect(result.levels).toEqual(expected.levels); - }); - it("should truncate a level description", () => { - const md = `# Title + it("should truncate a level description", () => { + const md = `# Title Description. @@ -145,27 +147,27 @@ Description. Some text that becomes the summary and goes beyond the maximum length of 80 so that it gets truncated at the end `; - const skeleton = { levels: [{ id: "L1" }] }; - const result = parse({ - text: md, - skeleton, - commits: {}, + const skeleton = { levels: [{ id: "L1" }] }; + const result = parse({ + text: md, + skeleton, + commits: {}, + }); + const expected = { + levels: [ + { + id: "L1", + title: "Put Level's title here", + summary: "Some text that becomes the summary", + content: "Some text that becomes the summary", + }, + ], + }; + expect(result.levels[0].summary).toMatch(/\.\.\.$/); }); - const expected = { - levels: [ - { - id: "L1", - title: "Put Level's title here", - summary: "Some text that becomes the summary", - content: "Some text that becomes the summary", - }, - ], - }; - expect(result.levels[0].summary).toMatch(/\.\.\.$/); - }); - it("should match line breaks with double line breaks for proper spacing", () => { - const md = `# Title + it("should match line breaks with double line breaks for proper spacing", () => { + const md = `# Title Description. @@ -180,30 +182,30 @@ Second line Third line `; - const skeleton = { levels: [{ id: "L1" }] }; - const result = parse({ - text: md, - skeleton, - commits: {}, - }); - const expected = { - summary: { - description: "Description.\n\nSecond description line", - }, - levels: [ - { - id: "L1", - summary: "Some text that becomes the summary", - content: "First line\n\nSecond line\n\nThird line", + const skeleton = { levels: [{ id: "L1" }] }; + const result = parse({ + text: md, + skeleton, + commits: {}, + }); + const expected = { + summary: { + description: "Description.\n\nSecond description line", }, - ], - }; - expect(result.summary.description).toBe(expected.summary.description); - expect(result.levels[0].content).toBe(expected.levels[0].content); - }); + levels: [ + { + id: "L1", + summary: "Some text that becomes the summary", + content: "First line\n\nSecond line\n\nThird line", + }, + ], + }; + expect(result.summary.description).toBe(expected.summary.description); + expect(result.levels[0].content).toBe(expected.levels[0].content); + }); - it("should load a single commit for a step", () => { - const md = `# Title + it("should load a single commit for a step", () => { + const md = `# Title Description. @@ -215,51 +217,51 @@ First line The first step `; - const skeleton = { - levels: [ - { - id: "L1", - steps: [ - { - id: "L1S1", - }, - ], - }, - ], - }; - const result = parse({ - text: md, - skeleton, - commits: { - L1S1Q: ["abcdefg1"], - }, - }); - const expected = { - summary: { - description: "Description.", - }, - levels: [ - { - id: "L1", - summary: "First line", - content: "First line", - steps: [ - { - id: "L1S1", - content: "The first step", - setup: { - commits: ["abcdefg1"], + const skeleton = { + levels: [ + { + id: "L1", + steps: [ + { + id: "L1S1", }, - }, - ], + ], + }, + ], + }; + const result = parse({ + text: md, + skeleton, + commits: { + L1S1Q: ["abcdefg1"], }, - ], - }; - expect(result.levels[0].steps[0]).toEqual(expected.levels[0].steps[0]); - }); + }); + const expected = { + summary: { + description: "Description.", + }, + levels: [ + { + id: "L1", + summary: "First line", + content: "First line", + steps: [ + { + id: "L1S1", + content: "The first step", + setup: { + commits: ["abcdefg1"], + }, + }, + ], + }, + ], + }; + expect(result.levels[0].steps[0]).toEqual(expected.levels[0].steps[0]); + }); - it("should load multiple commits for a step", () => { - const md = `# Title + it("should load multiple commits for a step", () => { + const md = `# Title Description. @@ -271,51 +273,51 @@ First line The first step `; - const skeleton = { - levels: [ - { - id: "L1", - steps: [ - { - id: "L1S1", - }, - ], - }, - ], - }; - const result = parse({ - text: md, - skeleton, - commits: { - L1S1Q: ["abcdefg1", "123456789"], - }, - }); - const expected = { - summary: { - description: "Description.", - }, - levels: [ - { - id: "L1", - summary: "First line", - content: "First line", - steps: [ - { - id: "L1S1", - content: "The first step", - setup: { - commits: ["abcdefg1", "123456789"], + const skeleton = { + levels: [ + { + id: "L1", + steps: [ + { + id: "L1S1", }, - }, - ], + ], + }, + ], + }; + const result = parse({ + text: md, + skeleton, + commits: { + L1S1Q: ["abcdefg1", "123456789"], }, - ], - }; - expect(result.levels[0].steps[0]).toEqual(expected.levels[0].steps[0]); - }); + }); + const expected = { + summary: { + description: "Description.", + }, + levels: [ + { + id: "L1", + summary: "First line", + content: "First line", + steps: [ + { + id: "L1S1", + content: "The first step", + setup: { + commits: ["abcdefg1", "123456789"], + }, + }, + ], + }, + ], + }; + expect(result.levels[0].steps[0]).toEqual(expected.levels[0].steps[0]); + }); - it("should load a single commit for a level", () => { - const md = `# Title + it("should load a single commit for a level", () => { + const md = `# Title Description. @@ -327,40 +329,92 @@ First line The first step `; - const skeleton = { - levels: [ - { - id: "L1", + const skeleton = { + levels: [ + { + id: "L1", + }, + ], + }; + const result = parse({ + text: md, + skeleton, + commits: { + L1: ["abcdefg1"], }, - ], - }; - const result = parse({ - text: md, - skeleton, - commits: { - L1: ["abcdefg1"], - }, + }); + const expected = { + summary: { + description: "Description.", + }, + levels: [ + { + id: "L1", + summary: "First line", + content: "First line", + setup: { + commits: ["abcdefg1"], + }, + }, + ], + }; + expect(result.levels[0].setup).toEqual(expected.levels[0].setup); }); - const expected = { - summary: { - description: "Description.", - }, - levels: [ - { - id: "L1", - summary: "First line", - content: "First line", - setup: { - commits: ["abcdefg1"], + + it("should load multi-line step content", () => { + const md = `# Title + +Description. + +## L1 Title + +First line + +### L1S1 + +The first step + +A codeblock: + +\`\`\`js +var a = 1; +\`\`\` + +Another line +`; + const skeleton = { + levels: [ + { + id: "L1", + steps: [ + { + id: "L1S1", + }, + ], }, + ], + }; + const result = parse({ + text: md, + skeleton, + commits: { + L1: ["abcdefg1"], + L1S1Q: ["12345678"], }, - ], - }; - expect(result.levels[0].setup).toEqual(expected.levels[0].setup); - }); + }); + const expected = { + id: "L1S1", + setup: { + commits: ["12345678"], + }, + content: + "The first step\n\nA codeblock:\n\n```js\nvar a = 1;\n```\n\nAnother line", + }; + expect(result.levels[0].steps[0]).toEqual(expected); + }); - it("should load the full config for a step", () => { - const md = `# Title + it("should load the full config for a step", () => { + const md = `# Title Description. @@ -372,73 +426,73 @@ First line The first step `; - const skeleton = { - levels: [ - { - id: "L1", - steps: [ - { - id: "L1S1", - setup: { - commands: ["npm install"], - files: ["someFile.js"], - watchers: ["someFile.js"], - filter: "someFilter", - subtasks: true, - }, - solution: { - commands: ["npm install"], - files: ["someFile.js"], + const skeleton = { + levels: [ + { + id: "L1", + steps: [ + { + id: "L1S1", + setup: { + commands: ["npm install"], + files: ["someFile.js"], + watchers: ["someFile.js"], + filter: "someFilter", + subtasks: true, + }, + solution: { + commands: ["npm install"], + files: ["someFile.js"], + }, }, - }, - ], + ], + }, + ], + }; + const result = parse({ + text: md, + skeleton, + commits: { + L1S1Q: ["abcdefg1", "123456789"], + L1S1A: ["1gfedcba", "987654321"], }, - ], - }; - const result = parse({ - text: md, - skeleton, - commits: { - L1S1Q: ["abcdefg1", "123456789"], - L1S1A: ["1gfedcba", "987654321"], - }, - }); - const expected = { - summary: { - description: "Description.", - }, - levels: [ - { - id: "L1", - summary: "First line", - content: "First line", - steps: [ - { - id: "L1S1", - content: "The first step", - setup: { - commits: ["abcdefg1", "123456789"], - commands: ["npm install"], - files: ["someFile.js"], - watchers: ["someFile.js"], - filter: "someFilter", - subtasks: true, - }, - solution: { - commits: ["1gfedcba", "987654321"], - commands: ["npm install"], - files: ["someFile.js"], - }, - }, - ], + }); + const expected = { + summary: { + description: "Description.", }, - ], - }; - expect(result.levels[0].steps[0]).toEqual(expected.levels[0].steps[0]); - }); + levels: [ + { + id: "L1", + summary: "First line", + content: "First line", + steps: [ + { + id: "L1S1", + content: "The first step", + setup: { + commits: ["abcdefg1", "123456789"], + commands: ["npm install"], + files: ["someFile.js"], + watchers: ["someFile.js"], + filter: "someFilter", + subtasks: true, + }, + solution: { + commits: ["1gfedcba", "987654321"], + commands: ["npm install"], + files: ["someFile.js"], + }, + }, + ], + }, + ], + }; + expect(result.levels[0].steps[0]).toEqual(expected.levels[0].steps[0]); + }); - it("should load the full config for multiple levels & steps", () => { - const md = `# Title + it("should load the full config for multiple levels & steps", () => { + const md = `# Title Description. @@ -462,155 +516,155 @@ Second level content. The third step `; - const skeleton = { - levels: [ - { - id: "L1", - steps: [ - { - id: "L1S1", - setup: { - commands: ["npm install"], - files: ["someFile.js"], - watchers: ["someFile.js"], - filter: "someFilter", - subtasks: true, + const skeleton = { + levels: [ + { + id: "L1", + steps: [ + { + id: "L1S1", + setup: { + commands: ["npm install"], + files: ["someFile.js"], + watchers: ["someFile.js"], + filter: "someFilter", + subtasks: true, + }, + solution: { + commands: ["npm install"], + files: ["someFile.js"], + }, }, - solution: { - commands: ["npm install"], - files: ["someFile.js"], + { + id: "L1S2", + setup: { + commands: ["npm install"], + files: ["someFile.js"], + watchers: ["someFile.js"], + filter: "someFilter", + subtasks: true, + }, + solution: { + commands: ["npm install"], + files: ["someFile.js"], + }, }, - }, - { - id: "L1S2", - setup: { - commands: ["npm install"], - files: ["someFile.js"], - watchers: ["someFile.js"], - filter: "someFilter", - subtasks: true, - }, - solution: { - commands: ["npm install"], - files: ["someFile.js"], + ], + }, + { + id: "L2", + summary: "Second level content.", + content: "First level content.", + steps: [ + { + id: "L2S1", + setup: { + commands: ["npm install"], + files: ["someFile.js"], + watchers: ["someFile.js"], + filter: "someFilter", + subtasks: true, + }, + solution: { + commands: ["npm install"], + files: ["someFile.js"], + }, }, - }, - ], + ], + }, + ], + }; + const result = parse({ + text: md, + skeleton, + commits: { + L1S1Q: ["abcdef1", "123456789"], + L1S1A: ["1fedcba", "987654321"], + L1S2Q: ["2abcdef"], + L1S2A: ["3abcdef"], + L2S1Q: ["4abcdef"], + L2S1A: ["5abcdef"], }, - { - id: "L2", - summary: "Second level content.", - content: "First level content.", - steps: [ - { - id: "L2S1", - setup: { - commands: ["npm install"], - files: ["someFile.js"], - watchers: ["someFile.js"], - filter: "someFilter", - subtasks: true, - }, - solution: { - commands: ["npm install"], - files: ["someFile.js"], - }, - }, - ], + }); + const expected = { + summary: { + description: "Description.", }, - ], - }; - const result = parse({ - text: md, - skeleton, - commits: { - L1S1Q: ["abcdef1", "123456789"], - L1S1A: ["1fedcba", "987654321"], - L1S2Q: ["2abcdef"], - L1S2A: ["3abcdef"], - L2S1Q: ["4abcdef"], - L2S1A: ["5abcdef"], - }, - }); - const expected = { - summary: { - description: "Description.", - }, - levels: [ - { - id: "L1", - title: "Title 1", - summary: "First level content.", - content: "First level content.", - steps: [ - { - id: "L1S1", - content: "The first step", - setup: { - commits: ["abcdef1", "123456789"], - commands: ["npm install"], - files: ["someFile.js"], - watchers: ["someFile.js"], - filter: "someFilter", - subtasks: true, - }, - solution: { - commits: ["1fedcba", "987654321"], - commands: ["npm install"], - files: ["someFile.js"], - }, - }, - { - id: "L1S2", - content: "The second step", - setup: { - commits: ["2abcdef"], - commands: ["npm install"], - files: ["someFile.js"], - watchers: ["someFile.js"], - filter: "someFilter", - subtasks: true, - }, - solution: { - commits: ["3abcdef"], - commands: ["npm install"], - files: ["someFile.js"], + levels: [ + { + id: "L1", + title: "Title 1", + summary: "First level content.", + content: "First level content.", + steps: [ + { + id: "L1S1", + content: "The first step", + setup: { + commits: ["abcdef1", "123456789"], + commands: ["npm install"], + files: ["someFile.js"], + watchers: ["someFile.js"], + filter: "someFilter", + subtasks: true, + }, + solution: { + commits: ["1fedcba", "987654321"], + commands: ["npm install"], + files: ["someFile.js"], + }, }, - }, - ], - }, - { - id: "L2", - title: "Title 2", - summary: "Second level content.", - content: "Second level content.", - steps: [ - { - id: "L2S1", - content: "The third step", - setup: { - commits: ["4abcdef"], - commands: ["npm install"], - files: ["someFile.js"], - watchers: ["someFile.js"], - filter: "someFilter", - subtasks: true, + { + id: "L1S2", + content: "The second step", + setup: { + commits: ["2abcdef"], + commands: ["npm install"], + files: ["someFile.js"], + watchers: ["someFile.js"], + filter: "someFilter", + subtasks: true, + }, + solution: { + commits: ["3abcdef"], + commands: ["npm install"], + files: ["someFile.js"], + }, }, - solution: { - commits: ["5abcdef"], - commands: ["npm install"], - files: ["someFile.js"], + ], + }, + { + id: "L2", + title: "Title 2", + summary: "Second level content.", + content: "Second level content.", + steps: [ + { + id: "L2S1", + content: "The third step", + setup: { + commits: ["4abcdef"], + commands: ["npm install"], + files: ["someFile.js"], + watchers: ["someFile.js"], + filter: "someFilter", + subtasks: true, + }, + solution: { + commits: ["5abcdef"], + commands: ["npm install"], + files: ["someFile.js"], + }, }, - }, - ], - }, - ], - }; - expect(result.levels).toEqual(expected.levels); - }); + ], + }, + ], + }; + expect(result.levels).toEqual(expected.levels); + }); - it("should handle steps with no solution", () => { - const md = `# Title + it("should handle steps with no solution", () => { + const md = `# Title Description. @@ -623,193 +677,425 @@ First level content. The first step `; - const skeleton = { - levels: [ - { - id: "L1", - steps: [ + const skeleton = { + levels: [ + { + id: "L1", + steps: [ + { + id: "L1S1", + }, + ], + }, + ], + }; + const result = parse({ + text: md, + skeleton, + commits: { + L1S1Q: ["abcdef1", "123456789"], + }, + }); + const expected = { + summary: { + description: "Description.", + }, + levels: [ + { + id: "L1", + title: "Title 1", + summary: "First level content.", + content: "First level content.", + steps: [ + { + id: "L1S1", + content: "The first step", + setup: { + commits: ["abcdef1", "123456789"], + }, + }, + ], + }, + ], + }; + expect(result.levels).toEqual(expected.levels); + }); + }); + + describe("config", () => { + it("should parse the tutorial config", () => { + const md = `# Title + +Description. +`; + + const skeleton = { + config: { + testRunner: { + command: "./node_modules/.bin/mocha", + args: { + filter: "--grep", + tap: "--reporter=mocha-tap-reporter", + }, + directory: "coderoad", + setup: { + commands: [], + }, + }, + appVersions: { + vscode: ">=0.7.0", + }, + repo: { + uri: "https://path.to/repo", + branch: "aBranch", + }, + dependencies: [ { - id: "L1S1", + name: "node", + version: ">=10", }, ], }, - ], - }; - const result = parse({ - text: md, - skeleton, - commits: { - L1S1Q: ["abcdef1", "123456789"], - }, - }); - const expected = { - summary: { - description: "Description.", - }, - levels: [ - { - id: "L1", - title: "Title 1", - summary: "First level content.", - content: "First level content.", - steps: [ + }; + const result = parse({ + text: md, + skeleton, + commits: {}, + }); + const expected = { + summary: { + description: "Description.\n\nSecond description line", + }, + config: { + testRunner: { + command: "./node_modules/.bin/mocha", + args: { + filter: "--grep", + tap: "--reporter=mocha-tap-reporter", + }, + directory: "coderoad", + setup: { + commands: [], + }, + }, + repo: { + uri: "https://path.to/repo", + branch: "aBranch", + }, + dependencies: [ { - id: "L1S1", - content: "The first step", - setup: { - commits: ["abcdef1", "123456789"], - }, + name: "node", + version: ">=10", }, ], + appVersions: { + vscode: ">=0.7.0", + }, }, - ], - }; - expect(result.levels).toEqual(expected.levels); - }); + }; + expect(result.config).toEqual(expected.config); + }); - // config - it("should parse the tutorial config", () => { - const md = `# Title + it("should parse the tutorial config with INIT commits", () => { + const md = `# Title Description. `; - const skeleton = { - config: { - testRunner: { - command: "./node_modules/.bin/mocha", - args: { - filter: "--grep", - tap: "--reporter=mocha-tap-reporter", + const skeleton = { + config: { + testRunner: { + command: "./node_modules/.bin/mocha", + args: { + filter: "--grep", + tap: "--reporter=mocha-tap-reporter", + }, + directory: "coderoad", }, - directory: "coderoad", - setup: { - commands: [], + appVersions: { + vscode: ">=0.7.0", }, + repo: { + uri: "https://path.to/repo", + branch: "aBranch", + }, + dependencies: [ + { + name: "node", + version: ">=10", + }, + ], }, - appVersions: { - vscode: ">=0.7.0", + }; + const result = parse({ + text: md, + skeleton, + commits: { + INIT: ["abcdef1", "123456789"], }, - repo: { - uri: "https://path.to/repo", - branch: "aBranch", + }); + const expected = { + summary: { + description: "Description.\n\nSecond description line", }, - dependencies: [ - { - name: "node", - version: ">=10", + config: { + testRunner: { + command: "./node_modules/.bin/mocha", + args: { + filter: "--grep", + tap: "--reporter=mocha-tap-reporter", + }, + directory: "coderoad", + setup: { + commits: ["abcdef1", "123456789"], + }, }, - ], - }, - }; - const result = parse({ - text: md, - skeleton, - commits: {}, + repo: { + uri: "https://path.to/repo", + branch: "aBranch", + }, + dependencies: [ + { + name: "node", + version: ">=10", + }, + ], + appVersions: { + vscode: ">=0.7.0", + }, + }, + }; + expect(result.config).toEqual(expected.config); }); - const expected = { - summary: { - description: "Description.\n\nSecond description line", - }, - config: { - testRunner: { - command: "./node_modules/.bin/mocha", - args: { - filter: "--grep", - tap: "--reporter=mocha-tap-reporter", - }, - directory: "coderoad", - setup: { - commands: [], + }); + + describe("hints", () => { + it("should parse hints for a step", () => { + const md = `# Title + +Description. + +## L1 Title 1 + +First level content. + +### L1S1 + +The first step + +#### HINTS + +* First Hint +* Second Hint + +`; + const skeleton = { + levels: [ + { + id: "L1", + steps: [ + { + id: "L1S1", + }, + ], }, + ], + }; + const result = parse({ + text: md, + skeleton, + commits: { + L1S1Q: ["abcdef1", "123456789"], }, - repo: { - uri: "https://path.to/repo", - branch: "aBranch", + }); + const expected = { + summary: { + description: "Description.", }, - dependencies: [ + levels: [ { - name: "node", - version: ">=10", + id: "L1", + title: "Title 1", + summary: "First level content.", + content: "First level content.", + steps: [ + { + id: "L1S1", + content: "The first step", + setup: { + commits: ["abcdef1", "123456789"], + }, + hints: ["First Hint", "Second Hint"], + }, + ], }, ], - appVersions: { - vscode: ">=0.7.0", - }, - }, - }; - expect(result.config).toEqual(expected.config); - }); + }; + expect(result.levels).toEqual(expected.levels); + }); - it("should parse the tutorial config with INIT commits", () => { - const md = `# Title - + it("should parse hints for a step", () => { + const md = `# Title + Description. -`; - const skeleton = { - config: { - testRunner: { - command: "./node_modules/.bin/mocha", - args: { - filter: "--grep", - tap: "--reporter=mocha-tap-reporter", +## L1 Title 1 + +First level content. + +### L1S1 + +The first step + +#### HINTS + +* First Hint with \`markdown\`. See **bold** +* Second Hint has a codeblock + +\`\`\`js +var a = 1; +\`\`\` + +And spans multiple lines. +`; + const skeleton = { + levels: [ + { + id: "L1", + steps: [ + { + id: "L1S1", + }, + ], }, - directory: "coderoad", - }, - appVersions: { - vscode: ">=0.7.0", + ], + }; + const result = parse({ + text: md, + skeleton, + commits: { + L1S1Q: ["abcdef1", "123456789"], }, - repo: { - uri: "https://path.to/repo", - branch: "aBranch", + }); + const expected = { + summary: { + description: "Description.", }, - dependencies: [ + levels: [ { - name: "node", - version: ">=10", + id: "L1", + title: "Title 1", + summary: "First level content.", + content: "First level content.", + steps: [ + { + id: "L1S1", + content: "The first step", + setup: { + commits: ["abcdef1", "123456789"], + }, + hints: [ + "First Hint with `markdown`. See **bold**", + "Second Hint has a codeblock\n\n```js\nvar a = 1;\n```\n\nAnd spans multiple lines.", + ], + }, + ], }, ], - }, - }; - const result = parse({ - text: md, - skeleton, - commits: { - INIT: ["abcdef1", "123456789"], - }, + }; + expect(result.levels).toEqual(expected.levels); }); - const expected = { - summary: { - description: "Description.\n\nSecond description line", - }, - config: { - testRunner: { - command: "./node_modules/.bin/mocha", - args: { - filter: "--grep", - tap: "--reporter=mocha-tap-reporter", - }, - directory: "coderoad", - setup: { - commits: ["abcdef1", "123456789"], + + it("should parse hints and not interrupt next step", () => { + const md = `# Title + +Description. + +## L1 Title 1 + +First level content. + +### L1S1 + +The first step + +#### HINTS + +* First Hint with \`markdown\`. See **bold** +* Second Hint has a codeblock + +\`\`\`js +var a = 1; +\`\`\` + +And spans multiple lines. + +### L1S2 + +The second uninterrupted step +`; + const skeleton = { + levels: [ + { + id: "L1", + steps: [ + { + id: "L1S1", + }, + { + id: "L1S2", + }, + ], }, + ], + }; + const result = parse({ + text: md, + skeleton, + commits: { + L1S1Q: ["abcdef1"], + L1S1A: ["123456789"], + L1S2Q: ["fedcba1"], }, - repo: { - uri: "https://path.to/repo", - branch: "aBranch", + }); + const expected = { + summary: { + description: "Description.", }, - dependencies: [ + levels: [ { - name: "node", - version: ">=10", + id: "L1", + title: "Title 1", + summary: "First level content.", + content: "First level content.", + steps: [ + { + id: "L1S1", + content: "The first step", + setup: { + commits: ["abcdef1"], + }, + solution: { + commits: ["123456789"], + }, + hints: [ + "First Hint with `markdown`. See **bold**", + "Second Hint has a codeblock\n\n```js\nvar a = 1;\n```\n\nAnd spans multiple lines.", + ], + }, + { + id: "L1S2", + content: "The second uninterrupted step", + setup: { + commits: ["fedcba1"], + }, + }, + ], }, + {}, ], - appVersions: { - vscode: ">=0.7.0", - }, - }, - }; - expect(result.config).toEqual(expected.config); + }; + expect(result.levels[0]).toEqual(expected.levels[0]); + }); }); }); diff --git a/typings/tutorial.d.ts b/typings/tutorial.d.ts index efd08cf..f3fe401 100644 --- a/typings/tutorial.d.ts +++ b/typings/tutorial.d.ts @@ -28,6 +28,7 @@ export type Step = { setup: StepActions; solution: Maybe; subtasks?: { [testName: string]: boolean }; + hints?: string[]; }; /** A tutorial for use in VSCode CodeRoad */