diff --git a/.gitignore b/.gitignore index 48912d2..fb73d69 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ build -node_modules \ No newline at end of file +node_modules +coverage \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d681197 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Jest All", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": ["--runInBand"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "windows": { + "program": "${workspaceFolder}/node_modules/jest/bin/jest" + } + } + ] +} diff --git a/package.json b/package.json index 4d93191..9e008ab 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "moduleFileExtensions": [ "js", "ts" - ] + ], + "collectCoverage": true } } diff --git a/src/templates/coderoad.yaml b/src/templates/coderoad.yaml index 453f8e2..01c32f6 100644 --- a/src/templates/coderoad.yaml +++ b/src/templates/coderoad.yaml @@ -31,9 +31,8 @@ config: # - npm install ## App versions helps to ensure compatability with the Extension appVersions: - {} ## Ensure compatability with a minimal VSCode CodeRoad version - # vscode: '>=0.7.0' + vscode: ">=0.7.0" ## Repo information to load code from ## repo: @@ -62,25 +61,16 @@ levels: ## Setup for the first task. Required. setup: ## Files to open in a text editor when the task loads. Optional. - files: [] - # - package.json - ## Commits to load when the task loads. These should include failing tests. Required. - ## The list will be filled by the parser - commits: - [] - # - a commit hash + files: + - package.json ## Solution for the first task. Required. solution: ## Files to open when the solution loads. Optional. - files: [] - # - package.json - ## 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. - ## The list will be filled by the parser - commits: [] + files: + - package.json ## Example Two: Running commands - id: L1S2 setup: - commits: [] ## CLI commands that are run when the task loads. Optional. commands: - npm install @@ -94,8 +84,6 @@ levels: setup: files: - package.json - commits: - - commit7 ## Listeners that run tests when a file or directory changes. watchers: - package.json @@ -103,18 +91,11 @@ levels: solution: files: - package.json - commits: - - commit8 ## Example Four: Subtasks - id: L1S4 setup: - commits: - - commit8 commands: ## A filter is a regex that limits the test results - filter: "^Example 2" ## A feature that shows subtasks: all filtered active test names and the status of the tests (pass/fail). - subtasks: true - solution: - commits: - - commit9 diff --git a/src/utils/parse.ts b/src/utils/parse.ts index 6c66063..970ab99 100644 --- a/src/utils/parse.ts +++ b/src/utils/parse.ts @@ -4,6 +4,10 @@ import * as T from "../../typings/tutorial"; type TutorialFrame = { summary: T.TutorialSummary; + levels: { + [levelKey: string]: T.Level; + }; + steps: { [stepKey: string]: Partial }; }; export function parseMdContent(md: string): TutorialFrame | never { @@ -24,35 +28,35 @@ export function parseMdContent(md: string): TutorialFrame | never { } }); - const sections = {}; + const mdContent: TutorialFrame = { + summary: { + title: "", + description: "", + }, + levels: {}, + steps: {}, + }; - // Identify and remove the header + // Capture summary const summaryMatch = parts .shift() .match(/^#\s(?.*)[\n\r]+(?[^]*)/); - if (!summaryMatch.groups.tutorialTitle) { throw new Error("Missing tutorial title"); } + mdContent.summary.title = summaryMatch.groups.tutorialTitle.trim(); if (!summaryMatch.groups.tutorialDescription) { throw new Error("Missing tutorial summary description"); } - - sections["summary"] = { - title: summaryMatch.groups.tutorialTitle.trim(), - description: summaryMatch.groups.tutorialDescription.trim(), - }; + mdContent.summary.description = summaryMatch.groups.tutorialDescription.trim(); // Identify each part of the content - parts.forEach((section) => { + parts.forEach((section: string) => { + // match level const levelRegex = /^(##\s(?L\d+)\s(?.*)[\n\r]*(>\s*(?.*))?[\n\r]+(?[^]*))/; - const stepRegex = /^(###\s(?(?L\d+)S\d+)\s(?.*)[\n\r]+(?[^]*))/; - - const levelMatch = section.match(levelRegex); - const stepMatch = section.match(stepRegex); - - if (levelMatch) { + const levelMatch: RegExpMatchArray | null = section.match(levelRegex); + if (levelMatch && levelMatch.groups) { const { levelId, levelTitle, @@ -60,37 +64,30 @@ export function parseMdContent(md: string): TutorialFrame | never { levelContent, } = levelMatch.groups; - const level = { - [levelId]: { - id: levelId, - title: levelTitle, - summary: levelSummary - ? levelSummary.trim() - : _.truncate(levelContent, { length: 80, omission: "..." }), - content: levelContent.trim(), - }, - }; - - _.merge(sections, level); - } else if (stepMatch) { - const step = { - [stepMatch.groups.levelId]: { - steps: { - [stepMatch.groups.stepId]: { - id: stepMatch.groups.stepId, - // title: stepMatch.groups.stepTitle, //Not using at this momemnt - content: stepMatch.groups.stepContent.trim(), - }, - }, - }, + // @ts-ignore + mdContent.levels[levelId] = { + id: levelId, + title: levelTitle, + summary: levelSummary + ? levelSummary.trim() + : _.truncate(levelContent, { length: 80, omission: "..." }), + content: levelContent.trim(), }; - - _.merge(sections, step); + } else { + // match step + const stepRegex = /^(###\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(), + }; + } } }); - // @ts-ignore - return sections; + return mdContent; } type ParseParams = { @@ -100,65 +97,76 @@ type ParseParams = { }; export function parse(params: ParseParams): any { - const parsed = { ...params.config }; - const mdContent: TutorialFrame = parseMdContent(params.text); - // Add the summary to the tutorial file - parsed["summary"] = mdContent.summary; + const parsed: Partial = { + summary: mdContent.summary, + config: params.config.config, + levels: [], + }; // merge content and tutorial - if (parsed.levels) { - parsed.levels.forEach((level: T.Level, levelIndex: number) => { - const levelContent = mdContent[level.id]; + if (params.config.levels && params.config.levels.length) { + parsed.levels = params.config.levels.map( + (level: T.Level, levelIndex: number) => { + const levelContent = mdContent.levels[level.id]; + + if (!levelContent) { + console.log(`Markdown content not found for ${level.id}`); + return; + } - if (!levelContent) { - console.log(`Markdown content not found for ${level.id}`); - return; - } + level = { ...level, ...levelContent }; - // add level setup commits - const levelSetupKey = `L${levelIndex + 1}`; - if (params.commits[levelSetupKey]) { - if (!level.setup) { - level.setup = { - commits: [], - }; + // add level setup commits + const levelSetupKey = level.id; + if (params.commits[levelSetupKey]) { + if (!level.setup) { + level.setup = { + commits: [], + }; + } + level.setup.commits = params.commits[levelSetupKey]; } - level.setup.commits = params.commits[levelSetupKey]; - } - const { steps, ...content } = levelContent; - - // add level step commits - if (steps) { - steps.forEach((step: T.Step, stepIndex: number) => { - const stepSetupKey = `${levelSetupKey}S${stepIndex + `1`}Q`; - if (params.commits[stepSetupKey]) { - if (!step.setup) { - step.setup = { - commits: [], - }; + // 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: [], + }; + } + step.setup.commits = params.commits[stepSetupKey]; + } + + const stepSolutionKey = `${stepKey}A`; + if (params.commits[stepSolutionKey]) { + if (!step.solution) { + step.solution = { + commits: [], + }; + } + step.solution.commits = params.commits[stepSolutionKey]; } - step.setup.commits = params.commits[stepSetupKey]; - } - const stepSolutionKey = `${levelSetupKey}S${stepIndex + `1`}A`; - if (params.commits[stepSolutionKey]) { - if (!step.solution) { - step.solution = { - commits: [], - }; + // add markdown + const stepMarkdown: Partial = mdContent.steps[step.id]; + if (stepMarkdown) { + step = { ...step, ...stepMarkdown }; } - step.solution.commits = params.commits[stepSolutionKey]; + + step.id = `${stepKey}`; + return step; } + ); - return _.merge(step, steps[step.id]); - }); + return level; } - - _.merge(level, content); - }); + ); } return parsed; diff --git a/tests/parse.test.ts b/tests/parse.test.ts index 13ce2d2..bd290d6 100644 --- a/tests/parse.test.ts +++ b/tests/parse.test.ts @@ -1,6 +1,7 @@ import { parse } from "../src/utils/parse"; describe("parse", () => { + // summary it("should parse summary", () => { const md = `# Insert Tutorial's Title here @@ -23,6 +24,7 @@ describe("parse", () => { expect(result.summary).toEqual(expected.summary); }); + // levels it("should parse a level with no steps", () => { const md = `# Title @@ -52,6 +54,7 @@ Some text summary: "Level's summary: a short description of the level's content in one line.", content: "Some text", + steps: [], }, ], }; @@ -76,6 +79,7 @@ Some text id: "L1", setup: { files: [], commits: [] }, solution: { files: [], commits: [] }, + steps: [], }, ], }; @@ -94,6 +98,7 @@ Some text content: "Some text", setup: { files: [], commits: [] }, solution: { files: [], commits: [] }, + steps: [], }, ], }; @@ -123,6 +128,7 @@ Some text that becomes the summary title: "Put Level's title here", summary: "Some text that becomes the summary", content: "Some text that becomes the summary", + steps: [], }, ], }; @@ -196,31 +202,236 @@ Third line expect(result.levels[0].content).toBe(expected.levels[0].content); }); - it("should parse the tutorial config", () => { + it("should load a single commit for a step", () => { + const md = `# Title + +Description. + +## L1 Title + +First line + +### L1S1 Step + +The first step +`; + const config = { + levels: [ + { + id: "L1", + steps: [ + { + id: "L1S1", + }, + ], + }, + ], + }; + const result = parse({ + text: md, + config, + 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"], + }, + }, + ], + }, + ], + }; + expect(result.levels[0].steps[0]).toEqual(expected.levels[0].steps[0]); + }); + + it("should load multiple commits for a step", () => { const md = `# Title Description. + +## L1 Title + +First line + +### L1S1 Step + +The first step `; - const yaml = ` -config: - testRunner: - command: ./node_modules/.bin/mocha - args: - filter: --grep - tap: --reporter=mocha-tap-reporter - directory: coderoad - setup: - commits: - - abcdefg1 - commands: [] - appVersions: - vscode: '>=0.7.0' - repo: - uri: https://path.to/repo - branch: aBranch - dependencies: - - name: node - version: '>=10' + const config = { + levels: [ + { + id: "L1", + steps: [ + { + id: "L1S1", + }, + ], + }, + ], + }; + const result = parse({ + text: md, + config, + 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"], + }, + }, + ], + }, + ], + }; + expect(result.levels[0].steps[0]).toEqual(expected.levels[0].steps[0]); + }); + + it("should load a single commit for a level", () => { + const md = `# Title + +Description. + +## L1 Title + +First line + +### L1S1 + +The first step +`; + const config = { + levels: [ + { + id: "L1", + }, + ], + }; + const result = parse({ + text: md, + config, + 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); + }); + + it("should load the full config for a step", () => { + const md = `# Title + +Description. + +## L1 Title + +First line + +### L1S1 Step + +The first step +`; + const config = { + levels: [ + { + id: "L1", + steps: [ + { + id: "L1S1", + setup: { + commands: ["npm install"], + files: ["someFile.js"], + watchers: ["someFile.js"], + filter: "someFilter", + subtasks: true, + }, + }, + ], + }, + ], + }; + const result = parse({ + text: md, + config, + 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"], + commands: ["npm install"], + files: ["someFile.js"], + watchers: ["someFile.js"], + filter: "someFilter", + subtasks: true, + }, + }, + ], + }, + ], + }; + expect(result.levels[0].steps[0]).toEqual(expected.levels[0].steps[0]); + }); + + // config + it("should parse the tutorial config", () => { + const md = `# Title + +Description. `; const config = {