@@ -11,12 +9,16 @@ export const Hints = ({ hints }: { hints: string[] }) => {
};
const HintElement = ({ hint, i }: { hint: string; i: number }) => {
- const consoleMarkdown = `\nHint ${
- i + 1
- }
\n${hint}\n\n `;
+ const details = `Hint ${i + 1}
+
+ ${hint}`;
return (
-
+
+
+
);
};
diff --git a/.freeCodeCamp/client/components/test.tsx b/.freeCodeCamp/client/components/test.tsx
index 9070739a..b8b3ecf9 100644
--- a/.freeCodeCamp/client/components/test.tsx
+++ b/.freeCodeCamp/client/components/test.tsx
@@ -1,6 +1,5 @@
import { Loader } from './loader';
import { TestType } from '../types';
-import { parseMarkdown } from '../utils';
export const Test = ({ testText, passed, isLoading, testId }: TestType) => {
return (
@@ -10,7 +9,7 @@ export const Test = ({ testText, passed, isLoading, testId }: TestType) => {
);
diff --git a/.freeCodeCamp/client/index.tsx b/.freeCodeCamp/client/index.tsx
index e7cd727d..bcc41c32 100644
--- a/.freeCodeCamp/client/index.tsx
+++ b/.freeCodeCamp/client/index.tsx
@@ -10,7 +10,7 @@ import {
import { Loader } from './components/loader';
import { Landing } from './templates/landing';
import { Project } from './templates/project';
-import { parseMarkdown, parse } from './utils/index';
+import { parse } from './utils/index';
import { Header } from './components/header';
import './styles.css';
import { E44o5 } from './components/error';
@@ -129,7 +129,7 @@ const App = () => {
}
function updateDescription({ description }: { description: string }) {
- setDescription(parseMarkdown(description));
+ setDescription(description);
}
function updateTests({ tests }: { tests: TestType[] }) {
diff --git a/.freeCodeCamp/client/utils/index.ts b/.freeCodeCamp/client/utils/index.ts
index aade0731..e0032f49 100644
--- a/.freeCodeCamp/client/utils/index.ts
+++ b/.freeCodeCamp/client/utils/index.ts
@@ -14,7 +14,7 @@ marked.use(
})
);
-export function parseMarkdown(markdown: string) {
+function parseMarkdown(markdown: string) {
return marked.parse(markdown, { gfm: true });
}
diff --git a/.freeCodeCamp/plugin/index.js b/.freeCodeCamp/plugin/index.js
index 90871670..320d5136 100644
--- a/.freeCodeCamp/plugin/index.js
+++ b/.freeCodeCamp/plugin/index.js
@@ -1,3 +1,9 @@
+import { readFile } from 'fs/promises';
+import { freeCodeCampConfig, getState, ROOT } from '../tooling/env.js';
+import { CoffeeDown, parseMarkdown } from '../tooling/parser.js';
+import { join } from 'path';
+import { logover } from '../tooling/logger.js';
+
/**
* Project config from `config/projects.json`
* @typedef {Object} Project
@@ -24,6 +30,19 @@
* @property {boolean} isLoading
*/
+/**
+ * @typedef {Object} Lesson
+ * @property {string} description
+ * @property {[[string, string]]} tests
+ * @property {string[]} hints
+ * @property {[{filePath: string; fileSeed: string} | string]} seed
+ * @property {boolean?} isForce
+ * @property {string?} beforeAll
+ * @property {string?} afterAll
+ * @property {string?} beforeEach
+ * @property {string?} afterEach
+ */
+
export const pluginEvents = {
/**
* @param {Project} project
@@ -55,5 +74,78 @@ export const pluginEvents = {
/**
* @param {Project} project
*/
- onLessonFailed: async project => {}
+ onLessonFailed: async project => {},
+
+ /**
+ * @param {string} projectDashedName
+ * @returns {Promise<{title: string; description: string; numberOfLessons: number}>}
+ */
+ getProjectMeta: async projectDashedName => {
+ const { locale } = await getState();
+ const projectFilePath = join(
+ ROOT,
+ freeCodeCampConfig.curriculum.locales[locale],
+ projectDashedName + '.md'
+ );
+ const projectFile = await readFile(projectFilePath, 'utf8');
+ const coffeeDown = new CoffeeDown(projectFile);
+ const projectMeta = coffeeDown.getProjectMeta();
+ // Remove `` tags if present
+ const title = parseMarkdown(projectMeta.title).replace(/
|<\/p>/g, '');
+ const description = parseMarkdown(projectMeta.description);
+ const numberOfLessons = projectMeta.numberOfLessons;
+ return { title, description, numberOfLessons };
+ },
+
+ /**
+ * @param {string} projectDashedName
+ * @param {number} lessonNumber
+ * @returns {Promise} lesson
+ */
+ getLesson: async (projectDashedName, lessonNumber) => {
+ const { locale } = await getState();
+ const projectFilePath = join(
+ ROOT,
+ freeCodeCampConfig.curriculum.locales[locale],
+ projectDashedName + '.md'
+ );
+ const projectFile = await readFile(projectFilePath, 'utf8');
+ const coffeeDown = new CoffeeDown(projectFile);
+ const lesson = coffeeDown.getLesson(lessonNumber);
+ let seed = lesson.seed;
+ if (!seed.length) {
+ // Check for external seed file
+ const seedFilePath = projectFilePath.replace(/.md$/, '-seed.md');
+ try {
+ const seedContent = await readFile(seedFilePath, 'utf-8');
+ const coffeeDown = new CoffeeDown(seedContent);
+ seed = coffeeDown.getLesson(lessonNumber).seed;
+ } catch (e) {
+ if (e?.code !== 'ENOENT') {
+ logover.debug(e);
+ throw new Error(
+ `Error reading external seed for lesson ${lessonNumber}`
+ );
+ }
+ }
+ }
+ const { afterAll, afterEach, beforeAll, beforeEach, isForce } = lesson;
+ const description = parseMarkdown(lesson.description);
+ const tests = lesson.tests.map(([testText, test]) => [
+ parseMarkdown(testText),
+ test
+ ]);
+ const hints = lesson.hints.map(parseMarkdown);
+ return {
+ description,
+ tests,
+ hints,
+ seed,
+ beforeAll,
+ afterAll,
+ beforeEach,
+ afterEach,
+ isForce
+ };
+ }
};
diff --git a/.freeCodeCamp/tests/fixtures/expected-format.md b/.freeCodeCamp/tests/fixtures/expected-format.md
deleted file mode 100644
index 17a3ea89..00000000
--- a/.freeCodeCamp/tests/fixtures/expected-format.md
+++ /dev/null
@@ -1,74 +0,0 @@
-# Project Title
-
-Project description.
-
-## 0
-
-### --description--
-
-Some description.
-
-Maybe some code:
-
-```js
-const a = 1;
-// A comment at the end?
-```
-
-### --tests--
-
-Test text with many words.
-
-```js
-// First test code
-const a = 'test';
-```
-
-Second test text with `inline-code`.
-
-```js
-const a = 'test2';
-// Second test code;
-```
-
-### --seed--
-
-#### --"some/file.js"--
-
-```js
-const file = 'some';
-```
-
-#### --cmd--
-
-```bash
-echo "I am bash seed"
-```
-
-### --hints--
-
-#### 0
-
-Hint 1.
-
-#### 1
-
-Hint 2, with multiple lines.
-
-```js
-const a = 0;
-```
-
-### --before-all--
-
-```js
-global.__beforeAll = 'before-all';
-```
-
-### --before-each--
-
-```js
-global.__beforeEach = 'before-each';
-```
-
-## --fcc-end--
diff --git a/.freeCodeCamp/tests/fixtures/valid-poor-format.md b/.freeCodeCamp/tests/fixtures/valid-poor-format.md
deleted file mode 100644
index 46b172a0..00000000
--- a/.freeCodeCamp/tests/fixtures/valid-poor-format.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# Project Title
-Project description.
-## 0
-### --description--
-This description has no spaces between the heading.
-```rs
-
-//Same goes for this code.
-let mut a = 1;
-// comment
-```
-### --tests--
-Test text at top.
-```js
-// First test no space
-// No code?
-
-```
-
-Second test text with `inline-code`.
-
-
-```js
-// Too many spaces?
-const a = 'test2';
-```
-### --before-all--
-```js
-global.__beforeAll = 'before-all';
-```
-## --fcc-end--
\ No newline at end of file
diff --git a/.freeCodeCamp/tests/parser.test.js b/.freeCodeCamp/tests/parser.test.js
index e8c3a540..1643b3f7 100644
--- a/.freeCodeCamp/tests/parser.test.js
+++ b/.freeCodeCamp/tests/parser.test.js
@@ -1,21 +1,7 @@
/// Tests can be run from `self/`
-import {
- getProjectTitle,
- getLessonFromFile,
- getLessonDescription,
- getLessonTextsAndTests,
- getLessonSeed,
- getBeforeAll,
- getBeforeEach,
- getCommands,
- getFilesWithSeed,
- isForceFlag,
- extractStringFromCode,
- getLessonHints,
- getProjectDescription
-} from '../tooling/parser.js';
import { assert } from 'chai';
import { Logger } from 'logover';
+import { pluginEvents } from '../plugin/index.js';
const logover = new Logger({
debug: '\x1b[33m[parser.test]\x1b[0m',
@@ -24,116 +10,110 @@ const logover = new Logger({
timestamp: null
});
-const EXPECTED_PATH = '../.freeCodeCamp/tests/fixtures/expected-format.md';
-const POOR_PATH = '../.freeCodeCamp/tests/fixtures/valid-poor-format.md';
-
try {
- const projectTitle = await getProjectTitle(EXPECTED_PATH);
- assert.deepEqual(projectTitle, 'Project Title');
- const projectDescription = await getProjectDescription(EXPECTED_PATH);
- assert.deepEqual(projectDescription, 'Project description.');
- const lesson = await getLessonFromFile(EXPECTED_PATH, 0);
-
- const lessonDescription = getLessonDescription(lesson);
- assert.equal(
- lessonDescription,
- '\n\nSome description.\n\nMaybe some code:\n\n```js\nconst a = 1;\n// A comment at the end?\n```\n\n'
+ const {
+ title,
+ description: projectDescription,
+ numberOfLessons
+ } = await pluginEvents.getProjectMeta('build-x-using-y');
+ const {
+ description: lessonDescription,
+ tests,
+ hints,
+ seed,
+ isForce,
+ beforeAll,
+ beforeEach,
+ afterAll,
+ afterEach
+ } = await pluginEvents.getLesson('build-x-using-y', 0);
+
+ assert.deepEqual(title, 'Build X Using Y');
+ assert.deepEqual(
+ projectDescription,
+ 'In this course, you will build x using y.'
);
+ assert.deepEqual(numberOfLessons, 1);
- const lessonTextsAndTests = getLessonTextsAndTests(lesson);
+ assert.deepEqual(
+ lessonDescription,
+ `Some description here.
- assert.equal(lessonTextsAndTests[0][0], 'Test text with many words.');
- assert.equal(
- lessonTextsAndTests[0][1],
- "// First test code\nconst a = 'test';\n"
- );
- assert.equal(
- lessonTextsAndTests[1][0],
- 'Second test text with `inline-code`.'
- );
- assert.equal(
- lessonTextsAndTests[1][1],
- "const a = 'test2';\n// Second test code;\n"
- );
+\`\`\`rust
+fn main() {
+ println!("Hello, world!");
+}
+\`\`\`
- const lessonSeed = getLessonSeed(lesson);
+Here is an image:
- const lessonHints = getLessonHints(lesson);
+
- assert.equal(lessonHints.length, 2);
- assert.equal(lessonHints.at(0), 'Hint 1.');
- assert.equal(
- lessonHints.at(1),
- 'Hint 2, with multiple lines.\n\n```js\nconst a = 0;\n```'
+`
);
- const beforeAll = getBeforeAll(lesson);
- assert.equal(beforeAll, "global.__beforeAll = 'before-all';\n");
-
- const beforeEach = getBeforeEach(lesson);
- assert.equal(beforeEach, "global.__beforeEach = 'before-each';\n");
-
- const commands = getCommands(lessonSeed);
-
- const filesWithSeed = getFilesWithSeed(lessonSeed);
-
- const isForce = isForceFlag(lessonSeed);
-} catch (e) {
- throw logover.error(e);
-}
-
-// -----------------
-// VALID POOR FORMAT
-// -----------------
-
-try {
- const projectTitle = await getProjectTitle(POOR_PATH);
- assert.deepEqual(projectTitle, 'Project Title');
- const projectDescription = await getProjectDescription(EXPECTED_PATH);
- assert.deepEqual(projectDescription, 'Project description.');
- const lesson = await getLessonFromFile(POOR_PATH, 0);
-
- const lessonDescription = getLessonDescription(lesson);
- assert.equal(
- lessonDescription,
- '\nThis description has no spaces between the heading.\n```rs\n\n//Same goes for this code.\nlet mut a = 1;\n// comment\n```\n'
+ const expectedTests = [
+ [
+ 'First test using Chai.js `assert`.',
+ '// 0\n// Timeout for 3 seconds\nawait new Promise(resolve => setTimeout(resolve, 3000));\nassert.equal(true, true);'
+ ],
+ [
+ 'Second test using global variables passed from `before` hook.',
+ "// 1\nawait new Promise(resolve => setTimeout(resolve, 4000));\nassert.equal(__projectLoc, 'example global variable for tests');"
+ ],
+ [
+ 'Dynamic helpers should be imported.',
+ "// 2\nawait new Promise(resolve => setTimeout(resolve, 1000));\nassert.equal(__helpers.testDynamicHelper(), 'Helper success!');"
+ ]
+ ];
+
+ for (const [i, [testText, testCode]] of tests.entries()) {
+ assert.deepEqual(testText, expectedTests[i][0]);
+ assert.deepEqual(testCode, expectedTests[i][1]);
+ }
+
+ const expectedHints = [
+ 'Inline hint with `some` code `blocks`.\n\n',
+ 'Multi-line hint with:\n\n```js\nconst code_block = true;\n```\n\n'
+ ];
+
+ for (const [i, hint] of hints.entries()) {
+ assert.deepEqual(hint, expectedHints[i]);
+ }
+
+ const expectedSeed = [
+ {
+ filePath: 'build-x-using-y/readme.md',
+ fileSeed: '# Build X Using Y\n\nIn this course\n\n## 0\n\nHello'
+ },
+ 'npm install'
+ ];
+
+ let i = 0;
+ for (const s of seed) {
+ assert.deepEqual(s, expectedSeed[i]);
+ i++;
+ }
+ assert.deepEqual(i, 2);
+
+ assert.deepEqual(isForce, true);
+
+ assert.deepEqual(
+ beforeEach,
+ "await new Promise(resolve => setTimeout(resolve, 1000));\nconst __projectLoc = 'example global variable for tests';"
);
-
- const lessonTextsAndTests = getLessonTextsAndTests(lesson);
-
- assert.equal(lessonTextsAndTests[0][0], 'Test text at top.');
- assert.equal(
- lessonTextsAndTests[0][1],
- '// First test no space\n// No code?\n\n'
+ assert.deepEqual(
+ afterEach,
+ "await new Promise(resolve => setTimeout(resolve, 1000));\nlogover.info('after each');"
);
- assert.equal(
- lessonTextsAndTests[1][0],
- 'Second test text with `inline-code`.'
+ assert.deepEqual(
+ beforeAll,
+ "await new Promise(resolve => setTimeout(resolve, 1000));\nlogover.info('before all');"
);
- assert.equal(
- lessonTextsAndTests[1][1],
- "// Too many spaces?\nconst a = 'test2';\n"
+ assert.deepEqual(
+ afterAll,
+ "await new Promise(resolve => setTimeout(resolve, 1000));\nlogover.info('after all');"
);
-
- const lessonSeed = getLessonSeed(lesson);
-
- const beforeAll = getBeforeAll(lesson);
- assert.equal(beforeAll, "global.__beforeAll = 'before-all';\n");
-} catch (e) {
- throw logover.error(e);
-}
-
-try {
- let stringFromCode = extractStringFromCode(`\`\`\`js
-const a = 1;
-\`\`\``);
- assert.equal(stringFromCode, 'const a = 1;\n');
- stringFromCode = extractStringFromCode(`\`\`\`js
-const a = 1;
-// comment
-\`\`\`
-`);
- assert.equal(stringFromCode, 'const a = 1;\n// comment\n');
} catch (e) {
throw logover.error(e);
}
diff --git a/.freeCodeCamp/tooling/client-socks.js b/.freeCodeCamp/tooling/client-socks.js
index 6899efc8..39c121ff 100644
--- a/.freeCodeCamp/tooling/client-socks.js
+++ b/.freeCodeCamp/tooling/client-socks.js
@@ -1,3 +1,5 @@
+import { parseMarkdown } from './parser.js';
+
export function toggleLoaderAnimation(ws) {
ws.send(parse({ event: 'toggle-loader-animation' }));
}
@@ -24,7 +26,12 @@ export function updateTest(ws, test) {
* @param {string} description Lesson description
*/
export function updateDescription(ws, description) {
- ws.send(parse({ event: 'update-description', data: { description } }));
+ ws.send(
+ parse({
+ event: 'update-description',
+ data: { description }
+ })
+ );
}
/**
* Update the heading of the lesson
@@ -92,6 +99,16 @@ export function updateHints(ws, hints) {
* @param {{error: string; testText: string; passed: boolean;isLoading: boolean;testId: number;}} cons
*/
export function updateConsole(ws, cons) {
+ if (Object.keys(cons).length) {
+ if (cons.error) {
+ const error = `\`\`\`json\n${JSON.stringify(
+ cons.error,
+ null,
+ 2
+ )}\n\`\`\``;
+ cons.error = parseMarkdown(error);
+ }
+ }
ws.send(parse({ event: 'update-console', data: { cons } }));
}
@@ -130,7 +147,7 @@ export function parse(obj) {
* @param {WebSocket} ws WebSocket connection to the client
*/
export function resetBottomPanel(ws) {
- updateHints(ws, '');
+ updateHints(ws, []);
updateTests(ws, []);
updateConsole(ws, {});
}
diff --git a/.freeCodeCamp/tooling/env.js b/.freeCodeCamp/tooling/env.js
index 0aac9682..d69a4316 100644
--- a/.freeCodeCamp/tooling/env.js
+++ b/.freeCodeCamp/tooling/env.js
@@ -1,7 +1,7 @@
import { readFile, writeFile } from 'fs/promises';
import { join } from 'path';
import { logover } from './logger.js';
-import { getProjectTitle, getProjectDescription } from './parser.js';
+import { pluginEvents } from '../plugin/index.js';
export const ROOT = process.env.INIT_CWD || process.cwd();
@@ -77,14 +77,9 @@ export async function getProjectConfig(projectDashedName) {
const project = projects.find(p => p.dashedName === projectDashedName);
// Add title and description to project
- const { locale } = await getState();
- const projectFilePath = join(
- ROOT,
- freeCodeCampConfig.curriculum.locales[locale],
- project.dashedName + '.md'
+ const { title, description } = await pluginEvents.getProjectMeta(
+ projectDashedName
);
- const title = await getProjectTitle(projectFilePath);
- const description = await getProjectDescription(projectFilePath);
project.title = title;
project.description = description;
diff --git a/.freeCodeCamp/tooling/lesson.js b/.freeCodeCamp/tooling/lesson.js
index feead542..66c5933e 100644
--- a/.freeCodeCamp/tooling/lesson.js
+++ b/.freeCodeCamp/tooling/lesson.js
@@ -1,13 +1,5 @@
// This file parses answer files for lesson content
import { join } from 'path';
-import {
- getLessonFromFile,
- getLessonDescription,
- getLessonTextsAndTests,
- getProjectTitle,
- getLessonSeed,
- isForceFlag
-} from './parser.js';
import {
updateDescription,
updateProjectHeading,
@@ -29,16 +21,12 @@ import { pluginEvents } from '../plugin/index.js';
export async function runLesson(ws, projectDashedName) {
const project = await getProjectConfig(projectDashedName);
const { isIntegrated, dashedName, seedEveryLesson, currentLesson } = project;
- const { locale, lastSeed } = await getState();
- const projectFile = join(
- ROOT,
- freeCodeCampConfig.curriculum.locales[locale],
- dashedName + '.md'
- );
+ const { lastSeed } = await getState();
try {
- const lesson = await getLessonFromFile(projectFile, currentLesson);
-
- const description = getLessonDescription(lesson);
+ const { description, seed, isForce, tests } = await pluginEvents.getLesson(
+ projectDashedName,
+ currentLesson
+ );
if (currentLesson === 0) {
await pluginEvents.onProjectStart(project);
@@ -47,10 +35,9 @@ export async function runLesson(ws, projectDashedName) {
updateProject(ws, project);
if (!isIntegrated) {
- const textsAndTestsArr = getLessonTextsAndTests(lesson);
updateTests(
ws,
- textsAndTestsArr.reduce((acc, curr, i) => {
+ tests.reduce((acc, curr, i) => {
return [
...acc,
{ passed: false, testText: curr[0], testId: i, isLoading: false }
@@ -60,7 +47,7 @@ export async function runLesson(ws, projectDashedName) {
}
resetBottomPanel(ws);
- const title = await getProjectTitle(projectFile);
+ const { title } = await pluginEvents.getProjectMeta(projectDashedName);
updateProjectHeading(ws, {
title,
lessonNumber: currentLesson
@@ -74,9 +61,7 @@ export async function runLesson(ws, projectDashedName) {
(lastSeed?.projectDashedName === dashedName &&
lastSeed?.lessonNumber !== currentLesson)
) {
- const seed = getLessonSeed(lesson);
if (seed) {
- const isForce = isForceFlag(seed);
// force flag overrides seed flag
if ((seedEveryLesson && !isForce) || (!seedEveryLesson && isForce)) {
await seedLesson(ws, dashedName);
diff --git a/.freeCodeCamp/tooling/parser.js b/.freeCodeCamp/tooling/parser.js
index 5f4fb977..631b9b0d 100644
--- a/.freeCodeCamp/tooling/parser.js
+++ b/.freeCodeCamp/tooling/parser.js
@@ -1,333 +1,359 @@
-// This file contains the parser for the markdown lessons
-import { basename, join } from 'path';
-import { readFile } from 'fs/promises';
-import { createReadStream } from 'fs';
-import { createInterface } from 'readline';
-import { freeCodeCampConfig, getState, ROOT } from './env.js';
-import { logover } from './logger.js';
-
-const DESCRIPTION_MARKER = '### --description--';
-const TESTS_MARKER = '### --tests--';
-const SEED_MARKER = '### --seed--';
-const HINTS_MARKER = `### --hints--`;
-const BEFORE_ALL_MARKER = '### --before-all--';
-const AFTER_ALL_MARKER = '### --after-all--';
-const BEFORE_EACH_MARKER = '### --before-each--';
-const AFTER_EACH_MARKER = '### --after-each--';
-const NEXT_MARKER_REG = `\n###? --`;
-const CMD_MARKER = '#### --cmd--';
-const FILE_MARKER_REG = '(?<=#### --")[^"]+(?="--)';
+import { lexer } from 'marked';
/**
- * Reads the first line of the file to get the project name
- * @param {string} file - The relative path to the locale file
- * @returns {Promise<{projectTopic: string; currentProject: string}>} The project name
+ * A class that takes a Markdown string, uses the markedjs package to tokenize it, and provides convenience methods to access different tokens in the token tree
*/
-export async function getProjectTitle(file) {
- const readable = createReadStream(file);
- const reader = createInterface({ input: readable });
- const firstLine = await new Promise((resolve, reject) => {
- // Timeout after 1 second
- const timeout = setTimeout(() => {
- reader.close();
- readable.close();
- reject(new Error('Timeout'));
- }, 1000);
- reader
- .on('line', line => {
- reader.close();
- clearTimeout(timeout);
- resolve(line);
- })
- .on('error', err => {
- reader.close();
- clearTimeout(timeout);
- reject(err);
- });
- });
- readable.close();
- const proj = firstLine.replace('# ', '');
- if (!proj) {
- throw new Error('Invalid project title. See example format.');
+export class CoffeeDown {
+ constructor(tokensOrMarkdown, caller = null) {
+ this.caller = caller;
+ if (typeof tokensOrMarkdown == 'string') {
+ this.tokens = lexer(tokensOrMarkdown);
+ } else if (Array.isArray(tokensOrMarkdown)) {
+ this.tokens = tokensOrMarkdown;
+ } else {
+ this.tokens = [tokensOrMarkdown];
+ }
}
- return proj;
-}
-/**
- * Gets the project description
- * @param {string} file - The relative path to the locale file
- * @returns {Promise} The project description
- */
-export async function getProjectDescription(file) {
- const fileContent = await readFile(file, 'utf8');
- const fileContentSansCR = fileContent.replace(/\r/g, '');
- const projectDescription = fileContentSansCR.match(
- new RegExp(`#[^\n]+\n(.*?)\n## 0`, 's')
- )?.[1];
- if (!projectDescription) {
- throw new Error('Project description not found. See example format.');
- }
- return projectDescription.trim();
-}
+ getProjectMeta() {
+ // There should only be one H1 in the project which is the title
+ const title = this.tokens.find(
+ t => t.type === 'heading' && t.depth === 1
+ ).text;
+ // The first paragraph should be the description
+ const description = this.tokens.find(t => t.type === 'paragraph').text;
-/**
- * Gets all content within a lesson
- * @param {string} file - The relative path to the english locale file
- * @param {number} lessonNumber - The number of the lesson
- * @returns {Promise} The content of the lesson
- */
-export async function getLessonFromFile(file, lessonNumber = 0) {
- const fileContent = await readFile(file, 'utf8');
- const fileContentSansCR = fileContent.replace(/\r/g, '');
- const mat = fileContentSansCR.match(
- new RegExp(
- `## ${lessonNumber}\n(.*?)\n## (${lessonNumber + 1}|--fcc-end--)`,
- 's'
- )
- );
- const lesson = mat?.[1];
- if (!lesson) {
- logover.debug(`Lesson ${lessonNumber} not found in ${file}`);
- throw new Error(`Lesson ${lessonNumber} not found in ${file}`);
+ // All H2 elements with an integer for text are lesson headings
+ const numberOfLessons = this.tokens.filter(
+ t =>
+ t.type === 'heading' &&
+ t.depth === 2 &&
+ Number.isInteger(parseFloat(t.text))
+ ).length;
+ return { title, description, numberOfLessons };
}
- // Seed might be in external file, but still needs to be returned
- // as part of lesson.
- let fileSeedContent;
- try {
- const { locale } = await getState();
- const project = basename(file);
- const seedFile = join(
- ROOT,
- freeCodeCampConfig.curriculum.locales[locale],
- project.replace('.md', '-seed.md')
- );
- fileSeedContent = await readFile(seedFile, 'utf8');
- } catch (e) {
- if (e?.code !== 'ENOENT') {
- logover.debug(`Error reading external seed for lesson ${lessonNumber}`);
- logover.debug(e);
- throw new Error(`Error reading external seed for lesson ${lessonNumber}`);
+ getHeading(depth, text, caller) {
+ if (this.caller !== 'getLesson') {
+ throw new Error(
+ `${caller} must be called on getLesson. Called on ${this.caller}`
+ );
}
- }
- if (fileSeedContent) {
- const fileSeedContentSansCR = fileSeedContent.replace(/\r/g, '');
- const seed = fileSeedContentSansCR.match(
- // NOTE: For separate seed file, there is no condition for every lesson to
- // have a seed - lesson numbers are not necessarily sequential.
- new RegExp(`## ${lessonNumber}\n(.*?)\n## (\d+|--fcc-end--)`, 's')
- )?.[1];
- if (seed) {
- return lesson + seed;
+ const tokens = [];
+ let take = false;
+ for (const token of this.tokens) {
+ if (
+ token.type === 'heading' &&
+ token.depth <= depth &&
+ TOKENS.some(t => t.marker === token.text)
+ ) {
+ take = false;
+ }
+ if (take) {
+ tokens.push(token);
+ }
+ if (
+ token.type === 'heading' &&
+ token.depth === depth &&
+ token.text === text
+ ) {
+ take = true;
+ }
}
+ return new CoffeeDown(tokens, caller);
}
- return lesson;
-}
+ getLesson(lessonNumber) {
+ const lesson = this.#getLesson(lessonNumber);
+ const description = lesson.getDescription().markdown;
+ const tests = lesson.getTests().tests;
+ const seed = lesson.getSeed().seed;
+ const isForce = lesson
+ .getSeed()
+ .tokens.some(
+ t => t.type === 'heading' && t.depth === 4 && t.text === '--force--'
+ );
+ const hints = lesson.getHints().hints;
+ const beforeAll = lesson.getBeforeAll().code;
+ const afterAll = lesson.getAfterAll().code;
+ const beforeEach = lesson.getBeforeEach().code;
+ const afterEach = lesson.getAfterEach().code;
+ return {
+ description,
+ tests,
+ seed,
+ hints,
+ beforeAll,
+ afterAll,
+ beforeEach,
+ afterEach,
+ isForce
+ };
+ }
-/**
- * Gets the description of the lesson
- * @param {string} lesson - The lesson content
- * @returns {string | null} The description of the lesson
- */
-export function getLessonDescription(lesson) {
- const description = parseMarker(DESCRIPTION_MARKER, lesson);
- return description ?? null;
-}
+ #getLesson(lessonNumber) {
+ const tokens = [];
+ let take = false;
+ for (const token of this.tokens) {
+ if (
+ token.type === 'heading' &&
+ token.depth === 2 &&
+ (parseInt(token.text, 10) === lessonNumber + 1 ||
+ token.text === '--fcc-end--')
+ ) {
+ take = false;
+ }
+ if (take) {
+ tokens.push(token);
+ }
+ if (token.type === 'heading' && token.depth === 2) {
+ if (parseInt(token.text, 10) === lessonNumber) {
+ take = true;
+ }
+ }
+ }
+ return new CoffeeDown(tokens, 'getLesson');
+ }
-/**
- * Gets the test text and tests of the lesson
- * @param {string} lesson - The lesson content
- * @returns {[string, string]} An array of [text, test]
- */
-export function getLessonTextsAndTests(lesson) {
- const testsString = parseMarker(TESTS_MARKER, lesson);
- const textsAndTestsArr = [];
- const texts = testsString?.match(/^(.*?)$(?=\n+```js)/gm)?.filter(Boolean);
- const tests = testsString?.match(/(?<=```js\n).*?(?=```)/gms);
+ getDescription() {
+ return this.getHeading(3, '--description--', 'getDescription');
+ }
- if (texts?.length) {
- for (let i = 0; i < texts.length; i++) {
- textsAndTestsArr.push([texts[i], tests[i]]);
- }
+ getTests() {
+ return this.getHeading(3, '--tests--', 'getTests');
}
- return textsAndTestsArr;
-}
-/**
- * Gets the seed of the lesson. If none is found, returns `null`.
- * @param {string} lesson - The lesson content
- * @returns {string | null} The seed of the lesson
- */
-export function getLessonSeed(lesson) {
- const seed = parseMarker(SEED_MARKER, lesson);
- return seed ?? null;
-}
+ getSeed() {
+ return this.getHeading(3, '--seed--', 'getSeed');
+ }
-/**
- * Gets the hints of the lesson. If none are found, returns `null`.
- * @param {string} lesson - The lesson content
- * @returns {string[]} The hints of the lesson
- */
-export function getLessonHints(lesson) {
- const hints = parseMarker(HINTS_MARKER, lesson);
- const hintsArr = hints?.split(/\n#### \d+/);
- return hintsArr?.map(h => h.trim()).filter(Boolean) ?? [];
-}
+ getHints() {
+ return this.getHeading(3, '--hints--', 'getHints');
+ }
-/**
- * Gets the command/script to run before running the lesson tests
- * @param {string} lesson - The lesson content
- * @returns {string | null} The command to run before running the lesson tests
- */
-export function getBeforeAll(lesson) {
- const beforeAll = parseMarker(BEFORE_ALL_MARKER, lesson);
- if (!beforeAll) return null;
- const beforeAllCommand = extractStringFromCode(beforeAll);
- return beforeAllCommand ?? null;
-}
+ getBeforeAll() {
+ return this.getHeading(3, '--before-all--', 'getBeforeAll');
+ }
-/**
- * Gets the command/script to run before running each lesson test
- * @param {string} lesson - The lesson content
- * @returns {string | null} The command to run before running each lesson test
- */
-export function getBeforeEach(lesson) {
- const beforeEach = parseMarker(BEFORE_EACH_MARKER, lesson);
- if (!beforeEach) return null;
- const beforeEachCommand = extractStringFromCode(beforeEach);
- return beforeEachCommand ?? null;
-}
+ getAfterAll() {
+ return this.getHeading(3, '--after-all--', 'getAfterAll');
+ }
-/**
- * Gets the command/script to run after running the lesson tests
- * @param {string} lesson - The lesson content
- * @returns {string} The command to run after running the lesson tests
- */
-export function getAfterAll(lesson) {
- const afterAll = parseMarker(AFTER_ALL_MARKER, lesson);
- if (!afterAll) return null;
- const afterAllCommand = extractStringFromCode(afterAll);
- return afterAllCommand ?? null;
-}
+ getBeforeEach() {
+ return this.getHeading(3, '--before-each--', 'getBeforeEach');
+ }
-/**
- * Gets the command/script to run after running each lesson test
- * @param {string} lesson - The lesson content
- * @returns {string | null} The command to run after running each lesson test
- */
-export function getAfterEach(lesson) {
- const beforeEach = parseMarker(AFTER_EACH_MARKER, lesson);
- if (!beforeEach) return null;
- const beforeEachCommand = extractStringFromCode(beforeEach);
- return beforeEachCommand;
-}
+ getAfterEach() {
+ return this.getHeading(3, '--after-each--', 'getAfterEach');
+ }
-/**
- * Gets any commands of the lesson seed. If none is found, returns an empty array.
- * @param {string} seed - The seed content
- * @returns {string[]} The commands of the lesson in order
- */
-export function getCommands(seed) {
- const cmds = seed.match(new RegExp(`${CMD_MARKER}\n(.*?\`\`\`\n)`, 'gs'));
- const commands = cmds?.map(cmd => extractStringFromCode(cmd)?.trim());
- return commands ?? [];
-}
+ /**
+ * Get first code block text from tokens
+ *
+ * Meant to be used with `getBeforeAll`, `getAfterAll`, `getBeforeEach`, and `getAfterEach`
+ */
+ get code() {
+ const callers = [
+ 'getBeforeAll',
+ 'getAfterAll',
+ 'getBeforeEach',
+ 'getAfterEach'
+ ];
+ if (!callers.includes(this.caller)) {
+ throw new Error(
+ `code must be called on "${callers.join(', ')}". Called on ${
+ this.caller
+ }`
+ );
+ }
+ return this.tokens.find(t => t.type === 'code')?.text;
+ }
-/**
- * Gets any seed for specified files of the lesson seed. If none is found, returns an empty array.
- * @param {string} seed - The seed content
- * @returns {[string, string][]} [[filePath, fileSeed]]
- */
-export function getFilesWithSeed(seed) {
- const files = seed.match(
- new RegExp(`#### --"([^"]+)"--\n(.*?\`\`\`\n)`, 'gs')
- );
- const filePaths = seed.match(new RegExp(FILE_MARKER_REG, 'gsm'));
- const fileSeeds = files?.map(file => extractStringFromCode(file)?.trim());
+ get seed() {
+ if (this.caller !== 'getSeed') {
+ throw new Error(
+ `seedToIterator must be called on getSeed. Called on ${this.caller}`
+ );
+ }
+ return seedToIterator(this.tokens);
+ }
- const pathAndSeedArr = [];
- if (filePaths?.length) {
- for (let i = 0; i < filePaths.length; i++) {
- pathAndSeedArr.push([filePaths[i], fileSeeds[i]]);
+ get tests() {
+ if (this.caller !== 'getTests') {
+ throw new Error(
+ `textsAndTests must be called on getTests. Called on ${this.caller}`
+ );
+ }
+ const textTokens = [];
+ const testTokens = [];
+ for (const token of this.tokens) {
+ if (token.type === 'paragraph') {
+ textTokens.push(token);
+ }
+ if (token.type === 'code') {
+ testTokens.push(token);
+ }
}
+ const texts = textTokens.map(t => t.text);
+ const tests = testTokens.map(t => t.text);
+ return texts.map((text, i) => [text, tests[i]]);
}
- return pathAndSeedArr;
-}
-/**
- * Returns `boolean` for if lesson seed contains `force` flag
- * @param {string} seed - The seed content
- * @returns {boolean} Whether the seed has the `force` flag
- */
-export function isForceFlag(seed) {
- return seed.includes('#### --force--');
-}
+ get hints() {
+ if (this.caller !== 'getHints') {
+ throw new Error(
+ `hints must be called on getHints. Called on ${this.caller}`
+ );
+ }
+ const hintTokens = [[]];
+ let currentHint = 0;
+ for (const token of this.tokens) {
+ if (token.type === 'heading' && token.depth === 4) {
+ if (token.text != currentHint) {
+ currentHint = token.text;
+ hintTokens[currentHint] = [];
+ }
+ } else {
+ hintTokens[currentHint].push(token);
+ }
+ }
+ const hints = hintTokens
+ .map(t => t.map(t => t.raw).join(''))
+ .filter(Boolean);
+ return hints;
+ }
-/**
- * Returns a string stripped from the input codeblock
- * @param {string} code - The codeblock to strip
- * @returns {string} The stripped codeblock
- */
-export function extractStringFromCode(code) {
- return code.replace(/.*?```[a-z]+\n(.*?)```.*/s, '$1');
-}
+ get markdown() {
+ return this.tokens.map(t => t.raw).join('');
+ }
-/**
- * Return the total number of lessons for a given project
- * @param {string} file - The relative path to the english locale file
- * @returns {Promise} The stripped codeblock
- */
-export async function getTotalLessons(file) {
- const fileContent = await readFile(file, 'utf-8');
- const lessonNumbers = fileContent.match(/\n## \d+/g);
- const numberOfLessons = lessonNumbers.length;
- return numberOfLessons;
+ get text() {
+ return this.tokens.map(t => t.text).join('');
+ }
}
-/**
- * Returns the content within the given marker of the lesson
- * @param {string} marker
- * @param {string} lesson
- * @returns {string | undefined} content or `undefined`
- *
- * **NOTE:** Immutably prepends lesson with `\n` if one does not already exist
- */
-function parseMarker(marker, lesson) {
- // Lesson must start with a new line, for RegExp to work,
- // and `\n` in RegExp cannot be removed, because it prevents
- // markers from being matched **within** rendered Markdown
- const lessonWithSlashN = lesson[0] === '\n' ? lesson : '\n' + lesson;
- const mat = lessonWithSlashN.match(
- new RegExp(`\n${marker}(((?!${NEXT_MARKER_REG}).)*\n?)`, 's')
- );
- return mat?.[1];
+function seedToIterator(tokens) {
+ const seed = [];
+ const sectionTokens = {};
+ let currentSection = 0;
+ for (const token of tokens) {
+ if (
+ token.type === 'heading' &&
+ token.depth === 4 &&
+ token.text !== '--force--'
+ ) {
+ if (token.text !== currentSection) {
+ currentSection = token.text;
+ sectionTokens[currentSection] = {};
+ }
+ } else if (token.type === 'code') {
+ sectionTokens[currentSection] = token;
+ }
+ }
+ for (const [filePath, { text }] of Object.entries(sectionTokens)) {
+ if (filePath === '--cmd--') {
+ seed.push(text);
+ } else {
+ seed.push({
+ filePath: filePath.slice(3, filePath.length - 3),
+ fileSeed: text
+ });
+ }
+ }
+ return seed;
}
-/**
- * Returns a generator on the seed for ordered execution
- * @param {string} seed The lesson seed
- */
-export function* seedToIterator(seed) {
- const sections = seed.match(new RegExp(`#### --(((?!#### --).)*\n?)`, 'sg'));
- for (const section of sections) {
- if (isForceFlag(section)) {
- continue;
- }
+import { marked } from 'marked';
+import { markedHighlight } from 'marked-highlight';
+import Prism from 'prismjs';
+import loadLanguages from 'prismjs/components/index.js';
- const isFile = section.match(
- new RegExp(`#### --"([^"]+)"--\n(.*?\`\`\`\n)`, 's')
- );
- const isCMD = section.match(new RegExp(`#### --cmd--\n(.*?\`\`\`\n)`, 's'));
- if (isFile) {
- const filePath = isFile[1];
- const fileSeed = extractStringFromCode(isFile[2])?.trim();
+loadLanguages([
+ 'javascript',
+ 'css',
+ 'html',
+ 'json',
+ 'markdown',
+ 'sql',
+ 'rust',
+ 'typescript',
+ 'jsx',
+ 'c',
+ 'csharp',
+ 'cpp',
+ 'dotnet',
+ 'python',
+ 'pug',
+ 'handlebars'
+]);
- yield { filePath, fileSeed };
- } else if (isCMD) {
- yield extractStringFromCode(isCMD[1])?.trim();
- } else {
- throw new Error('Seed is malformed');
+marked.use(
+ markedHighlight({
+ highlight: (code, lang) => {
+ if (Prism.languages[lang]) {
+ return Prism.highlight(code, Prism.languages[lang], String(lang));
+ } else {
+ return code;
+ }
}
- }
+ })
+);
+
+export function parseMarkdown(markdown) {
+ return marked.parse(markdown, { gfm: true });
}
+
+const TOKENS = [
+ {
+ marker: /\d+/,
+ depth: 2
+ },
+ {
+ marker: '--fcc-end--',
+ depth: 2
+ },
+ {
+ marker: '--description--',
+ depth: 3
+ },
+ {
+ marker: '--tests--',
+ depth: 3
+ },
+ {
+ marker: '--seed--',
+ depth: 3
+ },
+ {
+ marker: '--hints--',
+ depth: 3
+ },
+ {
+ marker: '--before-all--',
+ depth: 3
+ },
+ {
+ marker: '--after-all--',
+ depth: 3
+ },
+ {
+ marker: '--before-each--',
+ depth: 3
+ },
+ {
+ marker: '--after-each--',
+ depth: 3
+ },
+ {
+ marker: '--cmd--',
+ depth: 4
+ },
+ {
+ marker: /(?<=--)[^"]+(?="--)/,
+ depth: 4
+ },
+ {
+ marker: '--force--',
+ depth: 4
+ }
+];
diff --git a/.freeCodeCamp/tooling/reset.js b/.freeCodeCamp/tooling/reset.js
index 4f925ce7..295e8fa2 100644
--- a/.freeCodeCamp/tooling/reset.js
+++ b/.freeCodeCamp/tooling/reset.js
@@ -1,11 +1,9 @@
// Handles all the resetting of the projects
-
-import { join } from 'path';
import { resetBottomPanel, updateError } from './client-socks.js';
-import { getConfig, getProjectConfig, getState, ROOT } from './env.js';
+import { getProjectConfig, getState } from './env.js';
import { logover } from './logger.js';
-import { getLessonFromFile, getLessonSeed } from './parser.js';
import { runCommand, runLessonSeed } from './seed.js';
+import { pluginEvents } from '../plugin/index.js';
/**
* Resets the current project by running, in order, every seed
@@ -15,27 +13,18 @@ export async function resetProject(ws) {
resetBottomPanel(ws);
// Get commands and handle file setting
const { currentProject } = await getState();
- const freeCodeCampConfig = await getConfig();
const project = await getProjectConfig(currentProject);
const { currentLesson } = project;
- const FILE = join(
- ROOT,
- freeCodeCampConfig.curriculum.locales['english'],
- project.dashedName + '.md'
- );
let lessonNumber = 0;
try {
- let lesson = await getLessonFromFile(FILE, lessonNumber);
-
await gitResetCurrentProjectDir();
while (lessonNumber <= currentLesson) {
- const seed = getLessonSeed(lesson);
+ const { seed } = pluginEvents.getLesson(currentProject, lessonNumber);
if (seed) {
await runLessonSeed(seed, currentProject, lessonNumber);
}
lessonNumber++;
- lesson = await getLessonFromFile(FILE, lessonNumber);
}
} catch (err) {
updateError(ws, err);
diff --git a/.freeCodeCamp/tooling/seed.js b/.freeCodeCamp/tooling/seed.js
index 85e3ebd9..e5caf69c 100644
--- a/.freeCodeCamp/tooling/seed.js
+++ b/.freeCodeCamp/tooling/seed.js
@@ -1,6 +1,5 @@
// This file handles seeding the lesson contents with the seed in markdown.
import { join } from 'path';
-import { getLessonFromFile, getLessonSeed, seedToIterator } from './parser.js';
import {
ROOT,
getState,
@@ -14,6 +13,7 @@ import { exec } from 'child_process';
import { logover } from './logger.js';
import { updateError } from './client-socks.js';
import { watcher } from './hot-reload.js';
+import { pluginEvents } from '../plugin/index.js';
const execute = promisify(exec);
/**
@@ -24,17 +24,13 @@ const execute = promisify(exec);
export async function seedLesson(ws, projectDashedName) {
// TODO: Use ws to display loader whilst seeding
const project = await getProjectConfig(projectDashedName);
- const { currentLesson, dashedName } = project;
- const { locale } = await getState();
- const projectFile = join(
- ROOT,
- freeCodeCampConfig.curriculum.locales[locale],
- dashedName + '.md'
- );
+ const { currentLesson } = project;
try {
- const lesson = await getLessonFromFile(projectFile, currentLesson);
- const seed = getLessonSeed(lesson);
+ const { seed } = await pluginEvents.getLesson(
+ projectDashedName,
+ currentLesson
+ );
await runLessonSeed(seed, currentLesson);
await setState({
@@ -94,9 +90,8 @@ export async function runSeed(fileSeed, filePath) {
* @param {number} currentLesson
*/
export async function runLessonSeed(seed, currentLesson) {
- const seedGenerator = seedToIterator(seed);
try {
- for (const cmdOrFile of seedGenerator) {
+ for (const cmdOrFile of seed) {
if (typeof cmdOrFile === 'string') {
const { stdout, stderr } = await runCommand(cmdOrFile);
if (stdout || stderr) {
diff --git a/.freeCodeCamp/tooling/server.js b/.freeCodeCamp/tooling/server.js
index 2f8d2703..7cb37004 100644
--- a/.freeCodeCamp/tooling/server.js
+++ b/.freeCodeCamp/tooling/server.js
@@ -21,13 +21,9 @@ import { hotReload } from './hot-reload.js';
import { hideAll, showFile, showAll } from './utils.js';
import { join } from 'path';
import { logover } from './logger.js';
-import {
- getProjectDescription,
- getProjectTitle,
- getTotalLessons
-} from './parser.js';
import { resetProject } from './reset.js';
import { validateCurriculum } from './validate.js';
+import { pluginEvents } from '../plugin/index.js';
const freeCodeCampConfig = await getConfig();
@@ -112,16 +108,11 @@ async function getProjects() {
'utf-8'
)
);
- const { locale } = await getState();
for (const project of projects) {
- const projectFilePath = join(
- ROOT,
- freeCodeCampConfig.curriculum.locales[locale],
- project.dashedName + '.md'
+ const { title, description } = await pluginEvents.getProjectMeta(
+ project.dashedName
);
- const title = await getProjectTitle(projectFilePath);
- const description = await getProjectDescription(projectFilePath);
project.title = title;
project.description = description;
}
@@ -303,7 +294,9 @@ async function updateProjectConfig() {
freeCodeCampConfig.curriculum.locales['english'],
project.dashedName + '.md'
);
- const numberOfLessons = (await getTotalLessons(projectFilePath)) || 1;
+ const { numberOfLessons } = await pluginEvents.getProjectMeta(
+ project.dashedName
+ );
await setProjectConfig(project.dashedName, { numberOfLessons });
}
}
diff --git a/.freeCodeCamp/tooling/tests/main.js b/.freeCodeCamp/tooling/tests/main.js
index 07c6a8a4..d2f09b36 100644
--- a/.freeCodeCamp/tooling/tests/main.js
+++ b/.freeCodeCamp/tooling/tests/main.js
@@ -4,15 +4,6 @@ import { logover } from '../logger.js';
import { getProjectConfig, getState, setProjectConfig } from '../env.js';
import { freeCodeCampConfig, ROOT } from '../env.js';
-import {
- getLessonFromFile,
- getBeforeAll,
- getBeforeEach,
- getAfterAll,
- getAfterEach,
- getLessonHints,
- getLessonTextsAndTests
-} from '../parser.js';
import {
updateTest,
updateTests,
@@ -51,18 +42,14 @@ export async function runTests(ws, projectDashedName) {
const { locale } = await getState();
// toggleLoaderAnimation(ws);
const lessonNumber = project.currentLesson;
- const projectFile = join(
- ROOT,
- freeCodeCampConfig.curriculum.locales[locale],
- project.dashedName + '.md'
- );
+
let testsState = [];
try {
- const lesson = await getLessonFromFile(projectFile, lessonNumber);
- const beforeAll = getBeforeAll(lesson);
- const beforeEach = getBeforeEach(lesson);
- const afterAll = getAfterAll(lesson);
- const afterEach = getAfterEach(lesson);
+ const lesson = await pluginEvents.getLesson(
+ projectDashedName,
+ lessonNumber
+ );
+ const { beforeAll, beforeEach, afterAll, afterEach, hints, tests } = lesson;
if (beforeAll) {
try {
@@ -76,10 +63,7 @@ export async function runTests(ws, projectDashedName) {
}
// toggleLoaderAnimation(ws);
- const hints = getLessonHints(lesson);
-
- const textsAndTestsArr = getLessonTextsAndTests(lesson);
- testsState = textsAndTestsArr.map((text, i) => {
+ testsState = tests.map((text, i) => {
return {
passed: false,
testText: text[0],
@@ -96,7 +80,7 @@ export async function runTests(ws, projectDashedName) {
// Create one worker for each test if non-blocking.
// TODO: See if holding pool of workers is better.
if (project.blockingTests) {
- const worker = createWorker('blocking-worker', { beforeEach });
+ const worker = createWorker('blocking-worker', { beforeEach, project });
WORKER_POOL.push(worker);
// When result is received back from worker, update the client state
@@ -119,8 +103,8 @@ export async function runTests(ws, projectDashedName) {
});
});
- for (let i = 0; i < textsAndTestsArr.length; i++) {
- const [text, testCode] = textsAndTestsArr[i];
+ for (let i = 0; i < tests.length; i++) {
+ const [_text, testCode] = tests[i];
testsState[i].isLoading = true;
updateTest(ws, testsState[i]);
@@ -128,12 +112,12 @@ export async function runTests(ws, projectDashedName) {
}
} else {
// Run tests in parallel, and in own worker threads
- for (let i = 0; i < textsAndTestsArr.length; i++) {
- const [_text, testCode] = textsAndTestsArr[i];
+ for (let i = 0; i < tests.length; i++) {
+ const [_text, testCode] = tests[i];
testsState[i].isLoading = true;
updateTest(ws, testsState[i]);
- const worker = createWorker(`worker-${i}`, { beforeEach });
+ const worker = createWorker(`worker-${i}`, { beforeEach, project });
WORKER_POOL.push(worker);
// When result is received back from worker, update the client state
diff --git a/.freeCodeCamp/tooling/tests/test-worker.js b/.freeCodeCamp/tooling/tests/test-worker.js
index 484ab348..15fe7d4d 100644
--- a/.freeCodeCamp/tooling/tests/test-worker.js
+++ b/.freeCodeCamp/tooling/tests/test-worker.js
@@ -17,7 +17,7 @@ if (helpers) {
__helpers = { ...__helpers_c, ...dynamicHelpers };
}
-const { beforeEach = '' } = workerData;
+const { beforeEach = '', project } = workerData;
parentPort.on('message', async ({ testCode, testId }) => {
let passed = false;
diff --git a/.freeCodeCamp/tooling/validate.js b/.freeCodeCamp/tooling/validate.js
index c4cb2adf..2803e4f2 100644
--- a/.freeCodeCamp/tooling/validate.js
+++ b/.freeCodeCamp/tooling/validate.js
@@ -2,14 +2,7 @@ import { join } from 'path';
import { readdir, access, constants, readFile } from 'fs/promises';
import { freeCodeCampConfig, ROOT } from './env.js';
import { logover } from './logger.js';
-import {
- getLessonDescription,
- getLessonFromFile,
- getLessonTextsAndTests,
- getLessonSeed,
- getProjectTitle,
- getProjectDescription
-} from './parser.js';
+import { pluginEvents } from '../plugin/index.js';
const CURRICULUM_PATH = join(
ROOT,
@@ -241,15 +234,20 @@ export async function validateCurriculum() {
`Project "${project}" has a lesson number mismatch. Expected "${expectedLessonNumber}" but got "${lessonNumber}"`
);
}
- const lesson = await getLessonFromFile(projectPath, Number(lessonNumber));
- const description = getLessonDescription(lesson);
+ const {
+ description,
+ seed: seedContents,
+ tests: textsAndTests
+ } = await pluginEvents.getLesson(
+ project.dashedName,
+ Number(lessonNumber)
+ );
if (!description?.trim()) {
logover.warn(
`Project "${project}" has no description for lesson "${lessonNumber}"`
);
}
- const seedContents = getLessonSeed(lesson);
if (seed) {
const seedPath = join(CURRICULUM_PATH, seed);
const seedFile = await readFile(seedPath, 'utf8');
@@ -263,7 +261,6 @@ export async function validateCurriculum() {
}
}
- const textsAndTests = getLessonTextsAndTests(lesson);
if (!textsAndTests.length) {
logover.warn(
`Project "${project}" has no tests for lesson "${lessonNumber}"`
@@ -285,11 +282,11 @@ export async function validateCurriculum() {
expectedLessonNumber++;
}
- const projectTitle = await getProjectTitle(projectPath);
+ const { title: projectTitle, description: projectDescription } =
+ await pluginEvents.getProjectMeta(project.dashedName);
if (!projectTitle) {
throw new Error(`Project "${project}" has no title: '${projectTitle}'`);
}
- const projectDescription = await getProjectDescription(projectPath);
if (!projectDescription) {
throw new Error(
`Project "${project}" has no description: '${projectDescription}'`
diff --git a/.freeCodeCamp/webpack.config.cjs b/.freeCodeCamp/webpack.config.cjs
index fbff99d0..5d350484 100644
--- a/.freeCodeCamp/webpack.config.cjs
+++ b/.freeCodeCamp/webpack.config.cjs
@@ -38,7 +38,10 @@ module.exports = {
'c',
'csharp',
'cpp',
- 'dotnet'
+ 'dotnet',
+ 'python',
+ 'pug',
+ 'handlebars'
],
plugins: [],
theme: 'okaidia',
diff --git a/.gitpod.yml b/.gitpod.yml
index 8f26c0bd..372c5432 100644
--- a/.gitpod.yml
+++ b/.gitpod.yml
@@ -4,6 +4,7 @@ image:
# Commands to start on workspace startup
tasks:
- init: npm ci
+ - command: node tooling/adjust-url.js
ports:
- port: 8080
@@ -12,5 +13,5 @@ ports:
# TODO: See about publishing to Open VSX for smoother process
vscode:
extensions:
- - https://github.com/freeCodeCamp/courses-vscode-extension/releases/download/v2.1.0/freecodecamp-courses-2.1.0.vsix
+ - https://github.com/freeCodeCamp/courses-vscode-extension/releases/download/v3.0.0/freecodecamp-courses-3.0.0.vsix
- https://github.com/freeCodeCamp/freecodecamp-dark-vscode-theme/releases/download/v1.0.0/freecodecamp-dark-vscode-theme-1.0.0.vsix
diff --git a/.npmignore b/.npmignore
index 8dc92d0f..c65dba4a 100644
--- a/.npmignore
+++ b/.npmignore
@@ -1,8 +1,7 @@
-# Included, because it helps with testing when importing straight from GitHub
-# .freeCodeCamp/client
+.freeCodeCamp/client
.freeCodeCamp/tests
-# .freeCodeCamp/tsconfig.json
-# .freeCodeCamp/webpack.config.cjs
+.freeCodeCamp/tsconfig.json
+.freeCodeCamp/webpack.config.cjs
.devcontainer
.github
diff --git a/a.md b/a.md
new file mode 100644
index 00000000..cd9b285e
--- /dev/null
+++ b/a.md
@@ -0,0 +1,109 @@
+# Heading
+
+Project description
+
+## 0
+
+### --description--
+
+## `Within Description`
+
+Hi.
+
+````markdown
+# Fake Heading
+
+Fake project description.
+
+```js
+const a = 1;
+```
+
+## 0
+
+### --description--
+
+Fake description.
+````
+
+Over multiple lines.
+
+### --tests--
+
+Some test text.
+
+```js
+const a = 1;
+```
+
+## 1
+
+### --description--
+
+Lesson 1 description.
+
+### --tests--
+
+Lesson 1 tests.
+
+```js
+assert.strictEqual(a, 1);
+```
+
+### --before-all--
+
+```js
+const a = 1;
+```
+
+### --after-all--
+
+```js
+const a = 2;
+```
+
+### --before-each--
+
+```js
+const a = 3;
+```
+
+### --after-each--
+
+```js
+const a = 4;
+```
+
+### --seed--
+
+#### --force--
+
+#### --cmd--
+
+```bash
+npm install
+```
+
+#### --"some/path/to/file.rs"--
+
+```rust
+fn main() {
+ println!("Hello, world!");
+}
+```
+
+### --hints--
+
+#### 0
+
+Hint 0. Some code:
+
+```rust
+const A: i32 = 1;
+```
+
+#### 1
+
+Hint 1. Inline `code`.
+
+## --fcc-end--
diff --git a/docs/src/CHANGELOG.md b/docs/src/CHANGELOG.md
index 71a54b0a..bbb8bbe0 100644
--- a/docs/src/CHANGELOG.md
+++ b/docs/src/CHANGELOG.md
@@ -1,5 +1,20 @@
# Changelog
+## [3.2.0] - 2024-02-12
+
+### Add
+
+- Parser API for curriculum files
+ - `pluginEvents.getProjectMeta`
+ - `pluginEvents.getLesson`
+
+### Fix
+
+- Refactor original regex Markdown parser to use `marked.lexer`
+ - This allows for more complex Markdown files to be parsed
+ - Render Markdown in server
+ - Pass HTML string to client to render
+
## [3.1.0] - 2024-02-05
### Add
diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md
index 5b00375f..4ce006c4 100644
--- a/docs/src/getting-started.md
+++ b/docs/src/getting-started.md
@@ -40,19 +40,6 @@ Add the following required configuration:
}
```
-````admonish attention
-Technically, the `develop-course` and `run-course` scripts have to be specific values (see below), but you can customize the command with other conditions.
-
-```json
-{
- "scripts": {
- "develop-course": "NODE_ENV=development node ./node_modules/@freecodecamp/freecodecamp-os/.freeCodeCamp/tooling/server.js",
- "run-course": "NODE_ENV=production node ./node_modules/@freecodecamp/freecodecamp-os/.freeCodeCamp/tooling/server.js"
- }
-}
-```
-````
-
````admonish example collapsible=true
```json
{
diff --git a/docs/src/plugin-system.md b/docs/src/plugin-system.md
index df3720bd..09f91970 100644
--- a/docs/src/plugin-system.md
+++ b/docs/src/plugin-system.md
@@ -30,6 +30,60 @@ Called when a lesson passes, after all tests are run **and** passed, and only ha
Called when a lesson fails, after all tests are run **and** any fail.
+## Parser
+
+It is possible to define a custom parser for the curriculum files. This is useful when the curriculum files are not in the default format described in the [project syntax](./project-syntax.md) section.
+
+The first parameter of the parser functions is the project dashed name. This is the same as the `dashedName` field in the `projects.json` file.
+
+It is up to the parser to read, parse, and return the data in the format expected by the application.
+
+### `getProjectMeta`
+
+```ts
+(projectDashedName: string) =>
+ Promise<{
+ title: string;
+ description: string;
+ numberOfLessons: number;
+ }>;
+```
+
+The `title` and `description` fields are expected to be either plain strings, or HTML strings which are then rendered in the client.
+
+### `getLesson`
+
+```admonish attention
+This function can be called multiple times per lesson. Therefore, it is expected to be idempotent.
+```
+
+```typescript
+(projectDashedName: string, lessonNumber: number) =>
+ Promise<{
+ description: string;
+ tests: [[string, string]];
+ hints: string[];
+ seed: [{ filePath: string; fileSeed: string } | string];
+ isForce?: boolean;
+ beforeAll?: string;
+ afterAll?: string;
+ beforeEach?: string;
+ afterEach?: string;
+ }>;
+```
+
+The `description` field is expected to be either a plain string, or an HTML string which is then rendered in the client.
+
+The `tests[][0]` field is the test text, and the `tests[][1]` field is the test code. The test text is expected to be either a plain string, or an HTML string.
+
+The `hints` field is expected to be an array of plain strings, or an array of HTML strings.
+
+The `seed[].filePath` field is the relative path to the file from the workspace root. The `seed[].fileSeed` field is the file content to be written to the file.
+
+The `seed[]` field can also be a plain string, which is then treated as a `bash` command to be run in the workspace root.
+
+An example of this can be seen in the default parser used:
+
## Example
````admonish example title=" "
diff --git a/docs/src/project-syntax.md b/docs/src/project-syntax.md
index acff302e..3d105d55 100644
--- a/docs/src/project-syntax.md
+++ b/docs/src/project-syntax.md
@@ -1,6 +1,6 @@
# Project Syntax
-This is the Markdown syntax used to create projects in the curriculum.
+This is the Markdown syntax used to create projects in the curriculum using the default parser. The parser can be configured using the [plugin-system](./plugin-system.md).
## Markers
diff --git a/docs/src/roadmap.md b/docs/src/roadmap.md
index 173a4008..5b12b98f 100644
--- a/docs/src/roadmap.md
+++ b/docs/src/roadmap.md
@@ -8,4 +8,3 @@ For the most part, this roadmap outlines todos for `freecodecamp-os`. If this ro
- [ ] Loader to show progress of "Reset Step"
- [ ] Crowdin translation integration
- - Possibly, this will include UI to select available languages
diff --git a/docs/theme/css/general.css b/docs/theme/css/general.css
index 1ac74757..290c15a9 100644
--- a/docs/theme/css/general.css
+++ b/docs/theme/css/general.css
@@ -100,15 +100,15 @@ h6:target::before {
.page {
outline: 0;
padding: 0 var(--page-padding);
- margin-top: calc(
- 0px - var(--menu-bar-height)
- ); /* Compensate for the #menu-bar-hover-placeholder */
+ margin-top: calc(0px - var(--menu-bar-height));
+ /* Compensate for the #menu-bar-hover-placeholder */
}
.page-wrapper {
box-sizing: border-box;
}
.js:not(.sidebar-resizing) .page-wrapper {
- transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
+ transition: margin-left 0.3s ease, transform 0.3s ease;
+ /* Animation: slide away */
}
.content {
@@ -213,10 +213,10 @@ kbd {
visibility: hidden;
color: #fff;
background-color: #333;
- transform: translateX(
- -50%
- ); /* Center by moving tooltip 50% of its width left */
- left: -8px; /* Half of the width of the icon */
+ transform: translateX(-50%);
+ /* Center by moving tooltip 50% of its width left */
+ left: -8px;
+ /* Half of the width of the icon */
top: -35px;
font-size: 0.8em;
text-align: center;
@@ -238,3 +238,7 @@ kbd {
.result-no-output {
font-style: italic;
}
+
+dfn[title] {
+ text-decoration: underline dotted;
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 91117815..2c6a9ccc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,20 @@
{
"name": "@freecodecamp/freecodecamp-os",
- "version": "3.1.0",
+ "version": "3.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@freecodecamp/freecodecamp-os",
- "version": "3.1.0",
+ "version": "3.2.0",
"dependencies": {
"chai": "4.4.1",
"chokidar": "3.6.0",
"express": "4.18.2",
"logover": "2.0.0",
+ "marked": "9.1.6",
+ "marked-highlight": "2.1.1",
+ "prismjs": "1.29.0",
"ws": "8.16.0"
},
"devDependencies": {
@@ -30,10 +33,7 @@
"css-loader": "6.10.0",
"file-loader": "6.2.0",
"html-webpack-plugin": "5.6.0",
- "marked": "9.1.6",
- "marked-highlight": "2.1.1",
"nodemon": "3.0.3",
- "prismjs": "1.29.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"style-loader": "3.3.4",
@@ -4754,7 +4754,6 @@
"version": "9.1.6",
"resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz",
"integrity": "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==",
- "dev": true,
"bin": {
"marked": "bin/marked.js"
},
@@ -4766,7 +4765,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/marked-highlight/-/marked-highlight-2.1.1.tgz",
"integrity": "sha512-ktdqwtBne8rim5mb+vvZ9FzElGFb+CHCgkx/g6DSzTjaSrVnxsJdSzB5YgCkknFrcOW+viocM1lGyIjC0oa3fg==",
- "dev": true,
"peerDependencies": {
"marked": ">=4 <13"
}
@@ -5418,7 +5416,6 @@
"version": "1.29.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
- "dev": true,
"engines": {
"node": ">=6"
}
diff --git a/package.json b/package.json
index 00aaa709..14f7b720 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "@freecodecamp/freecodecamp-os",
"author": "freeCodeCamp",
- "version": "3.1.0",
+ "version": "3.2.0",
"description": "Package used for freeCodeCamp projects with the freeCodeCamp Courses VSCode extension",
"scripts": {
"build:client": "NODE_ENV=production webpack --config ./.freeCodeCamp/webpack.config.cjs",
@@ -17,6 +17,9 @@
"chokidar": "3.6.0",
"express": "4.18.2",
"logover": "2.0.0",
+ "marked": "9.1.6",
+ "marked-highlight": "2.1.1",
+ "prismjs": "1.29.0",
"ws": "8.16.0"
},
"devDependencies": {
@@ -35,10 +38,7 @@
"css-loader": "6.10.0",
"file-loader": "6.2.0",
"html-webpack-plugin": "5.6.0",
- "marked": "9.1.6",
- "marked-highlight": "2.1.1",
"nodemon": "3.0.3",
- "prismjs": "1.29.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"style-loader": "3.3.4",
diff --git a/self/.vscode/settings.json b/self/.vscode/settings.json
index 0976bf2a..131aa602 100644
--- a/self/.vscode/settings.json
+++ b/self/.vscode/settings.json
@@ -17,7 +17,8 @@
"README.md": true,
"renovate.json": true,
"build-x-using-y": true,
- "learn-freecodecamp-os": false
+ "learn-freecodecamp-os": true,
+ "external-seed": false
},
"terminal.integrated.defaultProfile.linux": "bash",
"terminal.integrated.profiles.linux": {
diff --git a/self/config/projects.json b/self/config/projects.json
index 04b61b76..92d5f895 100644
--- a/self/config/projects.json
+++ b/self/config/projects.json
@@ -8,6 +8,9 @@
"runTestsOnWatch": true,
"seedEveryLesson": false,
"isResetEnabled": true,
+ "numberofLessons": null,
+ "blockingTests": null,
+ "breakOnFailure": null,
"numberOfLessons": 27
},
{
@@ -16,8 +19,26 @@
"isIntegrated": true,
"isPublic": true,
"currentLesson": 0,
- "numberOfLessons": 1,
+ "runTestsOnWatch": null,
+ "seedEveryLesson": null,
+ "isResetEnabled": null,
+ "numberofLessons": null,
"blockingTests": true,
- "breakOnFailure": false
+ "breakOnFailure": false,
+ "numberOfLessons": 1
+ },
+ {
+ "id": 2,
+ "dashedName": "external-seed",
+ "isIntegrated": false,
+ "isPublic": true,
+ "currentLesson": 0,
+ "runTestsOnWatch": false,
+ "seedEveryLesson": true,
+ "isResetEnabled": true,
+ "numberofLessons": null,
+ "blockingTests": false,
+ "breakOnFailure": false,
+ "numberOfLessons": 2
}
-]
\ No newline at end of file
+]
diff --git a/self/curriculum/locales/english/build-x-using-y.md b/self/curriculum/locales/english/build-x-using-y.md
index 15489fe4..6814c13b 100644
--- a/self/curriculum/locales/english/build-x-using-y.md
+++ b/self/curriculum/locales/english/build-x-using-y.md
@@ -43,7 +43,6 @@ Dynamic helpers should be imported.
// 2
await new Promise(resolve => setTimeout(resolve, 1000));
assert.equal(__helpers.testDynamicHelper(), 'Helper success!');
-// assert.fail('test');
```
### --before-each--
@@ -74,4 +73,40 @@ await new Promise(resolve => setTimeout(resolve, 1000));
logover.info('after all');
```
+### --hints--
+
+#### 0
+
+Inline hint with `some` code `blocks`.
+
+#### 1
+
+Multi-line hint with:
+
+```js
+const code_block = true;
+```
+
+### --seed--
+
+#### --force--
+
+#### --"build-x-using-y/readme.md"--
+
+```markdown
+# Build X Using Y
+
+In this course
+
+## 0
+
+Hello
+```
+
+#### --cmd--
+
+```bash
+npm install
+```
+
## --fcc-end--
diff --git a/self/curriculum/locales/english/external-seed-seed.md b/self/curriculum/locales/english/external-seed-seed.md
new file mode 100644
index 00000000..9bbed771
--- /dev/null
+++ b/self/curriculum/locales/english/external-seed-seed.md
@@ -0,0 +1,30 @@
+## 0
+
+### --seed--
+
+#### --cmd--
+
+```bash
+rm -f external-seed/index.js
+rm -f external-seed/log
+```
+
+## 1
+
+### --seed--
+
+#### --"external-seed/index.js"--
+
+```js
+const a = 'seeding works';
+console.log(a);
+```
+
+#### --cmd--
+
+```bash
+touch external-seed/log
+node external-seed/index.js > external-seed/log
+```
+
+## --fcc-end--
diff --git a/self/curriculum/locales/english/external-seed.md b/self/curriculum/locales/english/external-seed.md
new file mode 100644
index 00000000..246568dc
--- /dev/null
+++ b/self/curriculum/locales/english/external-seed.md
@@ -0,0 +1,47 @@
+# External Seed
+
+A project to test the default parser `external seed` feature.
+
+## 0
+
+### --description--
+
+The seed for this lesson deletes any `index.js` and `log` files within the `external-seed/` directory.
+
+### --tests--
+
+This test should pass, if the seed worked
+
+```js
+const { readdir } = await import('fs/promises');
+const dir = await readdir(join(ROOT, project.dashedName));
+assert.equal(
+ dir.length,
+ 1,
+ `"${project.dashedName}" is expected to only have the .gitkeep file.`
+);
+```
+
+## 1
+
+### --description--
+
+There should be a `index.js` file that was created and run when the lesson loaded.
+
+### --tests--
+
+The `index.js` file should be seeded for you.
+
+```js
+const { access, constants } = await import('fs/promises');
+await access(join(ROOT, project.dashedName, 'index.js'), constants.F_OK);
+```
+
+The `index.js` file should be run.
+
+```js
+const { access, constants } = await import('fs/promises');
+await access(join(ROOT, project.dashedName, 'log'), constants.F_OK);
+```
+
+## --fcc-end--
diff --git a/self/curriculum/locales/english/learn-freecodecamp-os.md b/self/curriculum/locales/english/learn-freecodecamp-os.md
index cd0517bf..dd0af04c 100644
--- a/self/curriculum/locales/english/learn-freecodecamp-os.md
+++ b/self/curriculum/locales/english/learn-freecodecamp-os.md
@@ -1,6 +1,6 @@
# Learn freeCodeCampOS
-In this course, you will learn how to use the @freecodecamp/freecodecamp-os package to develop courses.jy
+In this course, you will learn how to use the @freecodecamp/freecodecamp-os package to develop courses.
## 0
@@ -100,7 +100,7 @@ The `curriculum/locales/english/learn-freecodecamp-os.md` file should contain th
```js
const { readFile } = await import('fs/promises');
const file = await readFile(
- 'curriculum/locales/english/learn-freecodecamp-os.md',
+ join(ROOT, 'curriculum/locales/english/learn-freecodecamp-os.md'),
'utf-8'
);
assert.include(file.slice(0, 100), 'Welcome to freeCodeCampOS!');
@@ -175,7 +175,7 @@ try {
}
```
-Version `>=2` should be installed.
+Version `>=3` should be installed.
```js
try {
@@ -183,7 +183,7 @@ try {
'npm list',
project.dashedName
);
- assert.include(stdout, '@freecodecamp/freecodecamp-os@2');
+ assert.include(stdout, '@freecodecamp/freecodecamp-os@3');
} catch (e) {
assert.fail(e);
}
@@ -289,7 +289,7 @@ assert.equal(__projects[0].id, 0);
```js
const { readFile } = await import('fs/promises');
const file = await readFile(
- join(project.dashedName, 'config/projects.json'),
+ join(ROOT, project.dashedName, 'config/projects.json'),
'utf-8'
);
const __projects = JSON.parse(file);
@@ -391,8 +391,8 @@ try {
Add a title to the `learn-freecodecamp-os.md` file.
-```text
- # freeCodeCampOS Title
+```markdown
+# freeCodeCampOS Title
```
### --tests--
@@ -417,10 +417,10 @@ assert(file.startsWith(), '# freeCodeCampOS Title');
Add the first lesson to the `learn-freecodecamp-os.md` file, with a description heading:
-```text
- ## 0
+```markdown
+## 0
- ### --description--
+### --description--
```
### --tests--
@@ -459,8 +459,8 @@ assert(file.includes('\n### --description--'));
Signify the end of the file, by adding the following:
-```text
- ## --fcc-end--
+```markdown
+## --fcc-end--
```
### --tests--
@@ -507,10 +507,6 @@ Within the `freecodecamp.conf.json` file, add the following:
```json
{
"version": "0.0.1",
- "scripts": {
- "develop-course": "",
- "run-course": ""
- },
"config": {
"projects.json": "",
"state.json": ""
@@ -543,36 +539,6 @@ The `freecodecamp.conf.json` file should contain the `scripts` property.
assert.hasAllKeys(__conf, ['scripts']);
```
-The `scripts` property should be an object.
-
-```js
-assert.isObject(__conf.scripts);
-```
-
-The `scripts` property should contain the `develop-course` property.
-
-```js
-assert.hasAllKeys(__conf.scripts, ['develop-course']);
-```
-
-The `develop-course` property should be a string.
-
-```js
-assert.isString(__conf.scripts['develop-course']);
-```
-
-The `scripts` property should contain the `run-course` property.
-
-```js
-assert.hasAllKeys(__conf.scripts, ['run-course']);
-```
-
-The `run-course` property should be a string.
-
-```js
-assert.isString(__conf.scripts['run-course']);
-```
-
The `freecodecamp.conf.json` file should contain the `config` property.
```js
@@ -773,7 +739,7 @@ The terminal should have a warning about the first lesson description being empt
Fix this by adding the following text:
-```text
+```markdown
Welcome to freeCodeCampOS! 👋
This example project will walk you through some of the features of freeCodeCampOS, and how to use them for your own course.
@@ -832,16 +798,16 @@ Also, there should be a warning about the first lesson not having any tests.
Add a test by placing the 3rd-level heading `### --tests--` within the 2nd-level heading `## 0`:
-````txt
- ### --tests--
+````markdown
+### --tests--
- This is a test that will always fail.
+This is a test that will always fail.
- ```js
- assert.fail(
- 'This is a custom test assertion message. Click the > button to go to the next lesson'
- );
- ```
+```js
+assert.fail(
+ 'This is a custom test assertion message. Click the > button to go to the next lesson'
+);
+```
````
### --hints--
@@ -850,20 +816,20 @@ Add a test by placing the 3rd-level heading `### --tests--` within the 2nd-level
Tests take the form:
-````text
- ### --tests--
+````markdown
+### --tests--
-
+
- ```js
-
- ```
+```js
+
+```
-
+
- ```js
-
- ```
+```js
+
+```
````
#### 1
diff --git a/self/external-seed/.gitkeep b/self/external-seed/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/self/package-lock.json b/self/package-lock.json
index a82ad03f..0aa8007d 100644
--- a/self/package-lock.json
+++ b/self/package-lock.json
@@ -1,44 +1,45 @@
{
"name": "self",
- "version": "3.0.0",
+ "version": "3.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "self",
- "version": "3.0.0",
+ "version": "3.1.0",
"dependencies": {
"@freecodecamp/freecodecamp-os": "../"
}
},
"..": {
"name": "@freecodecamp/freecodecamp-os",
- "version": "3.0.0",
+ "version": "3.2.0",
"dependencies": {
- "@types/node": "18.19.9",
"chai": "4.4.1",
"chokidar": "3.5.3",
"express": "4.18.2",
"logover": "2.0.0",
- "nodemon": "3.0.3"
+ "marked": "9.1.6",
+ "marked-highlight": "2.1.1",
+ "ws": "8.16.0"
},
"devDependencies": {
- "@babel/core": "7.23.7",
+ "@babel/core": "7.23.9",
"@babel/plugin-syntax-import-assertions": "7.23.3",
- "@babel/preset-env": "7.23.8",
+ "@babel/preset-env": "7.23.9",
"@babel/preset-react": "7.23.3",
"@babel/preset-typescript": "7.23.3",
"@types/marked": "5.0.2",
+ "@types/node": "20.11.16",
"@types/prismjs": "1.26.3",
- "@types/react": "18.2.48",
+ "@types/react": "18.2.54",
"@types/react-dom": "18.2.18",
"babel-loader": "9.1.3",
"babel-plugin-prismjs": "2.1.0",
- "css-loader": "6.9.1",
+ "css-loader": "6.10.0",
"file-loader": "6.2.0",
"html-webpack-plugin": "5.6.0",
- "marked": "9.1.6",
- "marked-highlight": "2.1.0",
+ "nodemon": "3.0.3",
"prismjs": "1.29.0",
"react": "18.2.0",
"react-dom": "18.2.0",
diff --git a/self/package.json b/self/package.json
index e807077c..b969feed 100644
--- a/self/package.json
+++ b/self/package.json
@@ -2,7 +2,7 @@
"name": "self",
"private": true,
"author": "freeCodeCamp",
- "version": "3.1.0",
+ "version": "3.2.0",
"description": "Test repo for @freecodecamp/freecodecamp-os",
"scripts": {
"start": "node ./node_modules/@freecodecamp/freecodecamp-os/.freeCodeCamp/tooling/server.js"
diff --git a/self/tooling/adjust-url.js b/self/tooling/adjust-url.js
new file mode 100644
index 00000000..3114f5ff
--- /dev/null
+++ b/self/tooling/adjust-url.js
@@ -0,0 +1,37 @@
+//! This script adjusts the preview URL for freeCodeCamp - Courses to open the correct preview.
+import { readFile, writeFile } from 'fs/promises';
+
+let PREVIEW_URL = 'http://localhost:8080';
+if (process.env.GITPOD_WORKSPACE_URL) {
+ PREVIEW_URL = `https://8080-${
+ process.env.GITPOD_WORKSPACE_URL.split('https://')[1]
+ }`;
+} else if (process.env.CODESPACE_NAME) {
+ PREVIEW_URL = `https://${process.env.CODESPACE_NAME}-8080.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}`;
+}
+
+const VSCODE_SETTINGS_PATH = '.vscode/settings.json';
+
+async function main() {
+ const settings_file = await readFile(VSCODE_SETTINGS_PATH, 'utf-8');
+ const settings = JSON.parse(settings_file);
+
+ let [preview] = settings?.['freecodecamp-courses.workspace.previews'];
+ if (!preview.url) {
+ throw new Error('.vscode setting not found');
+ }
+ preview.url = PREVIEW_URL;
+
+ await writeFile(
+ VSCODE_SETTINGS_PATH,
+ JSON.stringify(settings, null, 2),
+ 'utf-8'
+ );
+}
+
+try {
+ main();
+} catch (e) {
+ console.error('Unable to adjust .vscode/settings.json preview url setting:');
+ console.error(e);
+}
diff --git a/self/tooling/plugins.js b/self/tooling/plugins.js
index 9380ae3f..d2fbe67f 100644
--- a/self/tooling/plugins.js
+++ b/self/tooling/plugins.js
@@ -1,25 +1,13 @@
import { pluginEvents } from '@freecodecamp/freecodecamp-os/.freeCodeCamp/plugin/index.js';
-pluginEvents.onTestsStart = async (project, testsState) => {
- console.log('onTestsStart');
-};
+pluginEvents.onTestsStart = async (project, testsState) => {};
-pluginEvents.onTestsEnd = async (project, testsState) => {
- console.log('onTestsEnd');
-};
+pluginEvents.onTestsEnd = async (project, testsState) => {};
-pluginEvents.onProjectStart = async project => {
- console.log('onProjectStart');
-};
+pluginEvents.onProjectStart = async project => {};
-pluginEvents.onProjectFinished = async project => {
- console.log('onProjectFinished');
-};
+pluginEvents.onProjectFinished = async project => {};
-pluginEvents.onLessonFailed = async project => {
- console.log('onLessonFailed');
-};
+pluginEvents.onLessonFailed = async project => {};
-pluginEvents.onLessonPassed = async project => {
- console.log('onLessonPassed');
-};
+pluginEvents.onLessonPassed = async project => {};
diff --git a/t.js b/t.js
new file mode 100644
index 00000000..5f034370
--- /dev/null
+++ b/t.js
@@ -0,0 +1,109 @@
+import { Renderer, Tokenizer, lexer, marked } from 'marked';
+import {
+ TokenTwister,
+ getLessonFromFile,
+ getProjectDescription
+} from './parser.js';
+import { readFile } from 'fs/promises';
+
+const options = {
+ async: false,
+ breaks: false,
+ gfm: true,
+ pedantic: false,
+ renderer: new Renderer(),
+ silent: false,
+ tokenizer: new Tokenizer(),
+ walkTokens: null
+};
+
+// const lexr = new marked.Lexer(options);
+
+// const tokens = lexr.lex(markdown);
+
+// console.log(tokens);
+// const lex = lexer(markdown);
+// console.log(JSON.stringify(lex, null, 2));
+
+const FILE = './a.md';
+
+const markdown = await readFile(FILE, 'utf8');
+
+const tokenTwister = new TokenTwister(markdown);
+
+// const projectTitle = tokenTwister.getHeading(1);
+// console.log('-'.repeat(10), 'PROJECT TITLE', '-'.repeat(10));
+// console.log(projectTitle.text);
+// console.log('-'.repeat(10), 'PROJECT TITLE', '-'.repeat(10));
+// const projectDescription = 'TODO';
+// console.log('-'.repeat(10), 'PROJECT DESCRIPTION', '-'.repeat(10));
+// console.log(projectDescription.markdown);
+// console.log('-'.repeat(10), 'PROJECT DESCRIPTION', '-'.repeat(10));
+const lesson_0 = tokenTwister.getLesson(0);
+console.log('-'.repeat(10), 'LESSON 0', '-'.repeat(10));
+console.log(lesson_0.markdown);
+console.log('-'.repeat(10), 'LESSON 0', '-'.repeat(10));
+// const description_0 = lesson_0.getDescription();
+// console.log('-'.repeat(10), 'DESCRIPTION 0', '-'.repeat(10));
+// console.log(description_0.markdown);
+// console.log('-'.repeat(10), 'DESCRIPTION 0', '-'.repeat(10));
+// const tests_0 = lesson_0.getTests();
+// console.log(tests_0.tokens);
+// console.log('-'.repeat(10), 'TESTS 0', '-'.repeat(10));
+// console.log(tests_0.markdown);
+// console.log('-'.repeat(10), 'TESTS 0', '-'.repeat(10));
+// const texts_and_tests_0 = tests_0.textsAndTests;
+// console.log(texts_and_tests_0);
+const seed_0 = lesson_0.getSeed();
+console.log('-'.repeat(10), 'SEED 0', '-'.repeat(10));
+console.log(seed_0.markdown);
+console.log('-'.repeat(10), 'SEED 0', '-'.repeat(10));
+const hints_0 = lesson_0.getHints();
+console.log('-'.repeat(10), 'HINTS 0', '-'.repeat(10));
+console.log(hints_0.markdown);
+console.log('-'.repeat(10), 'HINTS 0', '-'.repeat(10));
+const lesson_1 = tokenTwister.getLesson(1);
+console.log('-'.repeat(10), 'LESSON 1', '-'.repeat(10));
+console.log(lesson_1.markdown);
+console.log('-'.repeat(10), 'LESSON 1', '-'.repeat(10));
+// const description_1 = lesson_1.getDescription();
+// console.log('-'.repeat(10), 'DESCRIPTION 1', '-'.repeat(10));
+// console.log(description_1.markdown);
+// console.log('-'.repeat(10), 'DESCRIPTION 1', '-'.repeat(10));
+// const tests_1 = lesson_1.getTests();
+// console.log(tests_1.tokens);
+// console.log('-'.repeat(10), 'TESTS 1', '-'.repeat(10));
+// console.log(tests_1.markdown);
+// console.log('-'.repeat(10), 'TESTS 1', '-'.repeat(10));
+// const texts_and_tests_1 = tests_1.textsAndTests;
+// console.log(texts_and_tests_1);
+const seed_1 = lesson_1.getSeed();
+console.log('-'.repeat(10), 'SEED 1', '-'.repeat(10));
+console.log(seed_1.markdown);
+console.log('-'.repeat(10), 'SEED 1', '-'.repeat(10));
+console.log(seed_1);
+console.log(seed_1.seedToIterator);
+// const hints_1 = lesson_1.getHints();
+// console.log('-'.repeat(10), 'HINTS 1', '-'.repeat(10));
+// console.log(hints_1.markdown);
+// console.log('-'.repeat(10), 'HINTS 1', '-'.repeat(10));
+// const hints_markdown_1 = hints_1.hints;
+// console.log(hints_markdown_1);
+// const before_all_1 = lesson_1.getBeforeAll();
+// console.log('-'.repeat(10), 'BEFORE ALL 1', '-'.repeat(10));
+// console.log(before_all_1.markdown);
+// console.log('-'.repeat(10), 'BEFORE ALL 1', '-'.repeat(10));
+// const after_all_1 = lesson_1.getAfterAll();
+// console.log('-'.repeat(10), 'AFTER ALL 1', '-'.repeat(10));
+// console.log(after_all_1.markdown);
+// console.log('-'.repeat(10), 'AFTER ALL 1', '-'.repeat(10));
+// const before_each_1 = lesson_1.getBeforeEach();
+// console.log('-'.repeat(10), 'BEFORE EACH 1', '-'.repeat(10));
+// console.log(before_each_1.markdown);
+// console.log('-'.repeat(10), 'BEFORE EACH 1', '-'.repeat(10));
+// const after_each_1 = lesson_1.getAfterEach();
+// console.log('-'.repeat(10), 'AFTER EACH 1', '-'.repeat(10));
+// console.log(after_each_1.markdown);
+// console.log('-'.repeat(10), 'AFTER EACH 1', '-'.repeat(10));
+// console.log(before_all_1);
+// console.log(before_all_1.code);