diff --git a/package.json b/package.json index f67a83f..dfb904d 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,13 @@ }, "jest": { "preset": "ts-jest", - "testEnvironment": "node" + "verbose": true, + "testPathIgnorePatterns": [ + "build" + ], + "moduleFileExtensions": [ + "js", + "ts" + ] } } diff --git a/src/build.ts b/src/build.ts index 92119e6..ee16bc8 100644 --- a/src/build.ts +++ b/src/build.ts @@ -3,6 +3,7 @@ import * as path from "path"; import * as _ from "lodash"; import * as fs from "fs"; import * as T from "../typings/tutorial"; +import { parse } from "./utils/parse"; // import validate from './validator'; // import not working @@ -10,77 +11,6 @@ const simpleGit = require("simple-git/promise"); const workingDir = "tmp"; -type TutorialContent = {}; - -function parseContent(md: string): TutorialContent { - let start: number = -1; - const parts: any[] = []; - - const lines = md.split("\n"); - - // Split the multiple parts - This way enables the creator to use 4/5 level headers inside the content. - lines.forEach((line, index) => { - if (line.match(/#{1,3}\s/) || index === lines.length - 1) { - if (start !== -1) { - parts.push(lines.slice(start, index).join("\n")); - start = index; - } else { - start = index; - } - } - }); - - const sections = {}; - - // Identify and remove the header - const summaryMatch = parts - .shift() - .match(/^#\s(?.*)[\n\r]+(?[^]*)/); - - sections["summary"] = { - title: summaryMatch.groups.tutorialTitle.trim(), - description: summaryMatch.groups.tutorialDescription.trim(), - }; - - // Identify each part of the content - parts.forEach((section) => { - 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 level = { - [levelMatch.groups.levelId]: { - id: levelMatch.groups.levelId, - title: levelMatch.groups.levelTitle, - summary: levelMatch.groups.levelSummary.trim(), - content: levelMatch.groups.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(), - }, - }, - }, - }; - - _.merge(sections, step); - } - }); - - return sections; -} - function rmDir(dir: string, rmSelf = false) { try { let files; @@ -164,32 +94,10 @@ async function build({ repo, codeBranch, setupBranch, isLocal }: BuildOptions) { await git.checkout(setupBranch); // Load files - const _mdContent = fs.readFileSync( - path.join(localPath, "TUTORIAL.md"), - "utf8" - ); - let _tutorial = fs.readFileSync( - path.join(localPath, "coderoad.yaml"), - "utf8" - ); - - // Add one more line to the content as per Shawn's request - const mdContent: any = parseContent(_mdContent); - - // Parse tutorial to JSON - const tutorial: T.Tutorial = yamlParser.load(_tutorial); - - // Add the summary to the tutorial file - tutorial.summary = mdContent.summary; - - // merge content and tutorial - tutorial.levels.forEach((level: T.Level) => { - const { steps, ...content } = mdContent[level.id]; - - level.steps.forEach((step: T.Step) => _.merge(step, steps[step.id])); + const _content = fs.readFileSync(path.join(localPath, "TUTORIAL.md"), "utf8"); + let _config = fs.readFileSync(path.join(localPath, "coderoad.yaml"), "utf8"); - _.merge(level, content); - }); + const tutorial = parse(_content, _config); // Checkout the code branches await git.checkout(codeBranch); diff --git a/src/templates/coderoad.yaml b/src/templates/coderoad.yaml index 7c06ac6..654c148 100644 --- a/src/templates/coderoad.yaml +++ b/src/templates/coderoad.yaml @@ -2,16 +2,7 @@ # This is a YAML-formatted file. ## Your personal version of the tutorial ## -version: '0.1.0' -## Data used to populate the tutorial summary page -## -summary: - ## The title of your tutorial. Required. - ## - title: '' - ## A description of your tutorial. Required. - ## - description: '' +version: "0.1.0" ## Data used to configure and setup the tutorial ## config: @@ -32,13 +23,15 @@ config: setup: ## A list of commits to load to setup the tutorial commits: [] - # - commit1 - # - commit2 + # - commit1 + # - commit2 ## A list of commands to run to configure the tutorial - commands: [] + commands: + [] # - npm install ## App versions helps to ensure compatability with the Extension - appVersions: {} + appVersions: + {} ## Ensure compatability with a minimal VSCode CodeRoad version # vscode: '>=0.7.0' ## Repo information to load code from @@ -46,14 +39,15 @@ config: repo: ## The uri path to the repo containing the code commits. Required. ## - uri: '' + uri: "" ## The branch on the repo uri that contains the code commits. Required. - branch: '' - + branch: "" + ## A list of tutorial dependencies to ensure the environment is setup for the tutorial. Optional. ## The dependencies will be checked by running `dependency.name` --version and comparing it to the version provided. ## - dependencies: [] + dependencies: + [] ## The name of the dependency # - name: node # ## The version requirement. See https://github.com/npm/node-semver for options. @@ -69,16 +63,17 @@ levels: setup: ## Files to open in a text editor when the task loads. Optional. files: [] - # - package.json + # - package.json ## Commits to load when the task loads. These should include failing tests. Required. ## The list will be filled by the parser - commits: [] + commits: + [] # - a commit hash ## Solution for the first task. Required. solution: ## Files to open when the solution loads. Optional. files: [] - # - package.json + # - 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: [] @@ -117,10 +112,9 @@ levels: - commit8 commands: ## A filter is a regex that limits the test results - - filter: '^Example 2' + - 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 - \ No newline at end of file diff --git a/src/utils/parse.ts b/src/utils/parse.ts new file mode 100644 index 0000000..e182b10 --- /dev/null +++ b/src/utils/parse.ts @@ -0,0 +1,114 @@ +import * as yamlParser from "js-yaml"; +import * as _ from "lodash"; +import * as T from "../../typings/tutorial"; + +type TutorialFrame = { + summary: T.TutorialSummary; +}; + +export function parseMdContent(md: string): TutorialFrame | never { + let start: number = -1; + const parts: any[] = []; + + const lines = md.split("\n"); + + // Split the multiple parts - This way enables the creator to use 4/5 level headers inside the content. + lines.forEach((line, index) => { + if (line.match(/#{1,3}\s/) || index === lines.length - 1) { + if (start !== -1) { + parts.push(lines.slice(start, index).join("\n")); + start = index; + } else { + start = index; + } + } + }); + + const sections = {}; + + // Identify and remove the header + const summaryMatch = parts + .shift() + .match(/^#\s(?.*)[\n\r]+(?[^]*)/); + + if (!summaryMatch.groups.tutorialTitle) { + throw new Error("Missing tutorial title"); + } + + if (!summaryMatch.groups.tutorialDescription) { + throw new Error("Missing tutorial summary description"); + } + + sections["summary"] = { + title: summaryMatch.groups.tutorialTitle.trim(), + description: summaryMatch.groups.tutorialDescription.trim(), + }; + + // Identify each part of the content + parts.forEach((section) => { + 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 level = { + [levelMatch.groups.levelId]: { + id: levelMatch.groups.levelId, + title: levelMatch.groups.levelTitle, + summary: levelMatch.groups.levelSummary.trim(), + content: levelMatch.groups.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(), + }, + }, + }, + }; + + _.merge(sections, step); + } + }); + + // @ts-ignore + return sections; +} + +export function parse(_content: string, _config: string): T.Tutorial { + const mdContent: TutorialFrame = parseMdContent(_content); + // Parse tutorial to JSON + const tutorial: T.Tutorial = yamlParser.load(_config); + + // Add the summary to the tutorial file + tutorial["summary"] = mdContent.summary; + + // merge content and tutorial + if (tutorial.levels) { + tutorial.levels.forEach((level: T.Level) => { + const levelContent = mdContent[level.id]; + if (!levelContent) { + console.log(`Markdown content not found for ${level.id}`); + return; + } + const { steps, ...content } = levelContent; + + if (steps) { + steps.forEach((step: T.Step) => _.merge(step, steps[step.id])); + } + + _.merge(level, content); + }); + } + + return tutorial; +} diff --git a/tests/parse.test.ts b/tests/parse.test.ts new file mode 100644 index 0000000..489b24c --- /dev/null +++ b/tests/parse.test.ts @@ -0,0 +1,52 @@ +import { parse } from "../src/utils/parse"; + +describe("parse", () => { + it("should parse summary", () => { + const md = `# Insert Tutorial's Title here + + Short description to be shown as a tutorial's subtitle. + + `; + + const yaml = `version: "0.1.0"`; + const result = parse(md, yaml); + 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); + }); + + it("should parse a level with a summary", () => { + 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 +`; + + const yaml = `version: "0.1.0" +levels: +- id: L1 +`; + const result = parse(md, yaml); + 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", + }, + ], + }; + expect(result.levels).toEqual(expected.levels); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index eb7d297..04d2d49 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,5 +22,5 @@ "removeComments": true, "skipLibCheck": true }, - "exclude": [".vscode", "bin", "build", "test"] + "exclude": ["node_modules", ".vscode", "bin", "build", "tests"] }