diff --git a/client/package.json b/client/package.json index 115e616821e..7469bba8a9a 100644 --- a/client/package.json +++ b/client/package.json @@ -43,6 +43,7 @@ "@fortawesome/free-brands-svg-icons": "6.1.1", "@fortawesome/free-solid-svg-icons": "6.1.1", "@fortawesome/react-fontawesome": "0.2.0", + "@freecodecamp/curriculum-helpers": "1.0.5", "@freecodecamp/loop-protect": "2.2.1", "@freecodecamp/react-bootstrap": "0.32.3", "@freecodecamp/react-calendar-heatmap": "1.0.0", diff --git a/client/src/client/frame-runner.ts b/client/src/client/frame-runner.ts index d950edc631a..7ce71c80e16 100644 --- a/client/src/client/frame-runner.ts +++ b/client/src/client/frame-runner.ts @@ -1,6 +1,6 @@ import '@babel/polyfill'; import jQuery from 'jquery'; -import curriculumHelpers from '../utils/curriculum-helpers'; +import * as helpers from '@freecodecamp/curriculum-helpers'; declare global { interface Window { @@ -86,7 +86,7 @@ async function initTestFrame(e: InitTestFrameArg = { code: {} }) { // eslint-disable-next-line no-inline-comments const { default: chai } = await import(/* webpackChunkName: "chai" */ 'chai'); const assert = chai.assert; - const __helpers = curriculumHelpers; + const __helpers = helpers; /* eslint-enable @typescript-eslint/no-unused-vars */ let Enzyme; diff --git a/client/src/client/workers/test-evaluator.ts b/client/src/client/workers/test-evaluator.ts index 974edce18ce..38fb0841c45 100644 --- a/client/src/client/workers/test-evaluator.ts +++ b/client/src/client/workers/test-evaluator.ts @@ -1,9 +1,7 @@ import chai from 'chai'; import '@babel/polyfill'; import { toString as __toString } from 'lodash-es'; -import curriculumHelpers, { - removeJSComments -} from '../../utils/curriculum-helpers'; +import * as helpers from '@freecodecamp/curriculum-helpers'; import { format as __format } from '../../utils/format'; const ctx: Worker & typeof globalThis = self as unknown as Worker & @@ -80,14 +78,14 @@ interface TestEvaluatorEvent extends MessageEvent { ctx.onmessage = async (e: TestEvaluatorEvent) => { /* eslint-disable @typescript-eslint/no-unused-vars */ let code = (e.data?.code?.contents || '').slice(); - code = e.data?.removeComments ? removeJSComments(code) : code; + code = e.data?.removeComments ? helpers.removeJSComments(code) : code; let editableContents = (e.data?.code?.editableContents || '').slice(); editableContents = e.data?.removeComments - ? removeJSComments(editableContents) + ? helpers.removeJSComments(editableContents) : editableContents; const assert = chai.assert; - const __helpers = curriculumHelpers; + const __helpers = helpers; // Fake Deep Equal dependency const DeepEqual = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b); @@ -105,7 +103,9 @@ ctx.onmessage = async (e: TestEvaluatorEvent) => { // Logging is proxyed after the build to catch console.log messages // generated during testing. testResult = (await eval(`${ - e.data?.removeComments ? removeJSComments(e.data.build) : e.data.build + e.data?.removeComments + ? helpers.removeJSComments(e.data.build) + : e.data.build } __utils.flushLogs(); __userCodeWasExecuted = true; diff --git a/client/src/utils/__fixtures/curriculum-helpers-css.ts b/client/src/utils/__fixtures/curriculum-helpers-css.ts deleted file mode 100644 index 8ac1bbc3f9d..00000000000 --- a/client/src/utils/__fixtures/curriculum-helpers-css.ts +++ /dev/null @@ -1,352 +0,0 @@ -const cssFullExample = ` -a { - color: green; - display: flex; -} -.aClass { - font-size: 32px; - /* the property below should not appear in final css string - width: 400px; - height: 200px; - */ - flex: 1; - flex-direction: row; -} -/* Set the background color to blue for screens that are 300px or less */ -@media screen and (max-width: 300px) { - body { - background-color: blue; - } -}`; - -const cssCodeWithCommentsRemoved = ` -a { - color: green; - display: flex; -} -.aClass { - font-size: 32px; - - flex: 1; - flex-direction: row; -} - -@media screen and (max-width: 300px) { - body { - background-color: blue; - } -}`; - -export const cssString = `:root { - --building-color1: #aa80ff; - --building-color2: #66cc99; - --building-color3: #cc6699; - --building-color4: #538cc6; - --window-color1: #bb99ff; - --window-color2: #8cd9b3; - --window-color3: #d98cb3; - --window-color4: #8cb3d9; -} - -* { - box-sizing: border-box; -} - -body { - height: 100vh; - margin: 0; - overflow: hidden; -} - -.background-buildings, -.foreground-buildings { - width: 100%; - height: 100%; - display: flex; - align-items: flex-end; - justify-content: space-evenly; - position: absolute; - top: 0; -} - -.building-wrap { - display: flex; - flex-direction: column; - align-items: center; -} - -.window-wrap { - display: flex; - align-items: center; - justify-content: space-evenly; -} - -.sky { - background: radial-gradient( - closest-corner circle at 15% 15%, - #ffcf33, - #ffcf33 20%, - #ffff66 21%, - #bbeeff 100% - ); -} - -/* BACKGROUND BUILDINGS - "bb" stands for "background building" */ -.bb1 { - width: 10%; - height: 70%; -} - -.bb1a { - width: 70%; -} - -.bb1b { - width: 80%; -} - -.bb1c { - width: 90%; -} - -.bb1d { - width: 100%; - height: 70%; - background: linear-gradient(var(--building-color1) 50%, var(--window-color1)); -} - -.bb1-window { - height: 10%; - background: linear-gradient(var(--building-color1), var(--window-color1)); -} - -.bb2 { - width: 10%; - height: 50%; -} - -.bb2a { - border-bottom: 5vh solid var(--building-color2); - border-left: 5vw solid transparent; - border-right: 5vw solid transparent; -} - -.bb2b { - width: 100%; - height: 100%; - background: repeating-linear-gradient( - var(--building-color2), - var(--building-color2) 6%, - var(--window-color2) 6%, - var(--window-color2) 9% - ); -} - -.bb3 { - width: 10%; - height: 55%; - background: repeating-linear-gradient( - 90deg, - var(--building-color3), - var(--building-color3), - var(--window-color3) 15% - ); -} - -.bb4 { - width: 11%; - height: 58%; -} - -.bb4a { - width: 3%; - height: 10%; - background-color: var(--building-color4); -} - -.bb4b { - width: 80%; - height: 5%; - background-color: var(--building-color4); -} - -.bb4c { - width: 100%; - height: 85%; - background-color: var(--building-color4); -} - -.bb4-window { - width: 18%; - height: 90%; - background-color: var(--window-color4); -} - -/* FOREGROUND BUILDINGS - "fb" stands for "foreground building" */ -.fb1 { - width: 10%; - height: 60%; -} - -.fb1a { - border-bottom: 7vh solid var(--building-color4); - border-left: 2vw solid transparent; - border-right: 2vw solid transparent; -} - -.fb1b { - width: 60%; - height: 10%; - background-color: var(--building-color4); -} - -.fb1c { - width: 100%; - height: 80%; - background: repeating-linear-gradient( - 90deg, - var(--building-color4), - var(--building-color4) 10%, - transparent 10%, - transparent 15% - ), - repeating-linear-gradient( - var(--building-color4), - var(--building-color4) 10%, - var(--window-color4) 10%, - var(--window-color4) 90% - ); -} - -.fb2 { - width: 10%; - height: 40%; -} - -.fb2a { - width: 100%; - border-bottom: 10vh solid var(--building-color3); - border-left: 1vw solid transparent; - border-right: 1vw solid transparent; -} - -.fb2b { - width: 100%; - height: 75%; - background-color: var(--building-color3); -} - -.fb2-window { - width: 22%; - height: 100%; - background-color: var(--window-color3); -} - -.fb3 { - width: 10%; - height: 35%; -} - -.fb3a { - width: 80%; - height: 15%; - background-color: var(--building-color1); -} - -.fb3b { - width: 100%; - height: 35%; - background-color: var(--building-color1); -} - -.fb3-window { - width: 25%; - height: 80%; - background-color: var(--window-color1); -} - -.fb4 { - width: 8%; - height: 45%; - position: relative; - left: 10%; -} - -.fb4a { - border-top: 5vh solid transparent; - border-left: 8vw solid var(--building-color1); -} - -.fb4b { - width: 100%; - height: 89%; - background-color: var(--building-color1); - display: flex; - flex-wrap: wrap; -} - -.fb4-window { - width: 30%; - height: 10%; - border-radius: 50%; - background-color: var(--window-color1); - margin: 10%; -} - -.fb5 { - width: 10%; - height: 33%; - position: relative; - right: 10%; - background: repeating-linear-gradient( - var(--building-color2), - var(--building-color2) 5%, - transparent 5%, - transparent 10% - ), - repeating-linear-gradient( - 90deg, - var(--building-color2), - var(--building-color2) 12%, - var(--window-color2) 12%, - var(--window-color2) 44% - ); -} - -.fb6 { - width: 9%; - height: 38%; - background: repeating-linear-gradient( - 90deg, - var(--building-color3), - var(--building-color3) 10%, - transparent 10%, - transparent 30% - ), - repeating-linear-gradient( - var(--building-color3), - var(--building-color3) 10%, - var(--window-color3) 10%, - var(--window-color3) 30% - ); -} - -@media (max-width: 1000px) { - .sky { - background: radial-gradient( - closest-corner circle at 15% 15%, - #ffcf33, - #ffcf33 20%, - #ffff66 21%, - #bbeeff 100% - ); - } -} -`; - -const testValues = { - cssFullExample, - cssCodeWithCommentsRemoved -}; - -export default testValues; diff --git a/client/src/utils/__fixtures/curriculum-helpers-html.ts b/client/src/utils/__fixtures/curriculum-helpers-html.ts deleted file mode 100644 index 0e8580fc8a5..00000000000 --- a/client/src/utils/__fixtures/curriculum-helpers-html.ts +++ /dev/null @@ -1,30 +0,0 @@ -const htmlFullExample = ` - - -not a comment - -not a commment not a comment -not a comment - -not a comment -`; - -const htmlCodeWithCommentsRemoved = ` - - -not a comment - -not a commment not a comment -not a comment - -not a comment -`; - -const testValues = { - htmlFullExample, - htmlCodeWithCommentsRemoved -}; - -export default testValues; diff --git a/client/src/utils/__fixtures/curriculum-helpers-javascript.ts b/client/src/utils/__fixtures/curriculum-helpers-javascript.ts deleted file mode 100644 index 1d11602a6a1..00000000000 --- a/client/src/utils/__fixtures/curriculum-helpers-javascript.ts +++ /dev/null @@ -1,70 +0,0 @@ -const jsCodeWithSingleAndMultLineComments = ` -function nonMutatingPush(original, newItem) { - /* This is a - multi-line comment - that should be removed. */ - return original.push(newItem); -} -var first = [1, 2, 3]; -// This is a single line comment -var second = [4, 5]; -nonMutatingPush(first, second);`; - -const jsCodeWithSingleAndMultLineCommentsRemoved = ` -function nonMutatingPush(original, newItem) { - return original.push(newItem); -} -var first = [1, 2, 3]; - -var second = [4, 5]; -nonMutatingPush(first, second);`; - -const jsCodeWithUrl = ` -function nonMutatingPush(original, newItem) { - var url = 'https://freecodecamp.org'; // this comment should vanish - return original.push(newItem); -}`; - -const jsCodeWithUrlUnchanged = ` -function nonMutatingPush(original, newItem) { - var url = 'https://freecodecamp.org'; - return original.push(newItem); -}`; - -const jsCodeWithNoCall = `function myFunc() { - return Math.random(); -} -`; - -const jsCodeWithNoArgCall = `function myFunc() { - return Math.random(); -} -myFunc(); -`; - -const jsCodeWithArgCall = `function myFunc() { - return Math.random(); -} -myFunc('this shouldn't be here'); -`; - -const jsCodeWithCommentedCall = `function myFunc() { - return Math.random(); -} -/* -myFunc(); -*/ -`; - -const testValues = { - jsCodeWithSingleAndMultLineComments, - jsCodeWithSingleAndMultLineCommentsRemoved, - jsCodeWithUrl, - jsCodeWithUrlUnchanged, - jsCodeWithNoCall, - jsCodeWithNoArgCall, - jsCodeWithArgCall, - jsCodeWithCommentedCall -}; - -export default testValues; diff --git a/client/src/utils/__fixtures/curriculum-helpers-remove-white-space.ts b/client/src/utils/__fixtures/curriculum-helpers-remove-white-space.ts deleted file mode 100644 index 1a5316047cc..00000000000 --- a/client/src/utils/__fixtures/curriculum-helpers-remove-white-space.ts +++ /dev/null @@ -1,14 +0,0 @@ -const stringWithWhiteSpaceChars = ` -This string sentence has various white spaces characters: -\t* This line starts with a tab character. -\t* This line has several preceding white space characters.`; - -const stringWithWhiteSpaceCharsRemoved = - 'Thisstringsentencehasvariouswhitespacescharacters:*Thislinestartswithatabcharacter.*Thislinehasseveralprecedingwhitespacecharacters.'; - -const testValues = { - stringWithWhiteSpaceChars, - stringWithWhiteSpaceCharsRemoved -}; - -export default testValues; diff --git a/client/src/utils/css-help.test.ts b/client/src/utils/css-help.test.ts deleted file mode 100644 index e59836b34ad..00000000000 --- a/client/src/utils/css-help.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { cssString } from './__fixtures/curriculum-helpers-css'; -import CSSHelp from './css-help'; - -describe('css-help', () => { - const doc = document; - let t: CSSHelp; - beforeEach(() => { - const style = doc.createElement('style'); - style.innerHTML = cssString as string; - doc.head.appendChild(style); - t = new CSSHelp(doc); - // JSDOM does not implement latest CSSOM spec. As such, - // conditionText property needs to be manually added. - // REF: https://github.com/freeCodeCamp/freeCodeCamp/pull/42148#issuecomment-847291137 - const mediaRule = t.getCSSRules('media')?.[0] as CSSMediaRule; - const conditionText = mediaRule.media[0]; - mediaRule.conditionText = conditionText; - }); - describe('getStyle', () => { - it('should return an ExtendedCSSStyleDeclartion object of length 1', () => { - expect(t.getStyle('*')?.length).toEqual(1); - }); - it('should return a non-empty ExtendedCSSStyleDeclaration object', () => { - expect(t.getStyle('.bb1')).toBeTruthy(); - }); - it('should return a whitespaceless string', () => { - expect(t.getStyle('.bb1d')?.getPropVal('background', true)).toEqual( - 'linear-gradient(var(--building-color1)50%,var(--window-color1))' - ); - }); - }); - describe('isPropertyUsed', () => { - it('should return true on existing properties', () => { - expect(t.isPropertyUsed('height')).toBeTruthy(); - }); - it('should return true on existing custom properties', () => { - expect(t.isPropertyUsed('--building-color1')).toBeTruthy(); - }); - }); - describe('isDeclaredAfter', () => { - it('should return true if existing style is declared after another', () => { - expect(t.getStyleRule('.bb1a')?.isDeclaredAfter('.bb1')).toBeTruthy(); - }); - }); - describe('getPropertyValue', () => { - it('should return custom property value needing trim', () => { - expect( - t.getStyle(':root')?.getPropertyValue('--building-color1')?.trim() - ).toEqual('#aa80ff'); - }); - it('should return value to existing property', () => { - expect( - t.getStyle('.bb4a')?.getPropertyValue('background-color') - ).toBeTruthy(); - }); - it('should return property value without evaluating result', () => { - expect(t.getStyle('.bb4a')?.getPropertyValue('background-color')).toEqual( - 'var(--building-color4)' - ); - }); - }); - describe('getCSSRules', () => { - it('should return a CSSRules array of length 1', () => { - expect(t.getCSSRules('media')?.length).toEqual(1); - }); - }); - describe('getRuleListsWithinMedia', () => { - it('should return a CSSMediaRule array with a selectable CSSStyleRule', () => { - expect( - t - .getRuleListsWithinMedia('(max-width: 1000px)') - .find(x => x.selectorText === '.sky') - ).toBeTruthy(); - }); - it('should return CSSStyleDeclaration property with complex value', () => { - // NOTE: JSDOM causes value to have tabbed characters, DOM has single-line values. - expect( - t - .getRuleListsWithinMedia('(max-width: 1000px)') - .find(x => x.selectorText === '.sky')?.style?.background - ).toEqual( - `radial-gradient( - closest-corner circle at 15% 15%, - #ffcf33, - #ffcf33 20%, - #ffff66 21%, - #bbeeff 100% - )` - ); - }); - }); - afterEach(() => { - document.body.innerHTML = ''; - document.head.innerHTML = ''; - }); -}); diff --git a/client/src/utils/css-help.ts b/client/src/utils/css-help.ts deleted file mode 100644 index 572903d55ae..00000000000 --- a/client/src/utils/css-help.ts +++ /dev/null @@ -1,128 +0,0 @@ -export interface ExtendedStyleRule extends CSSStyleRule { - isDeclaredAfter: (selector: string) => boolean; -} -interface ExtendedStyleDeclaration extends CSSStyleDeclaration { - getPropVal: (prop: string, strip?: boolean) => string; -} - -const getIsDeclaredAfter = (styleRule: CSSStyleRule) => (selector: string) => { - const cssStyleRules = Array.from( - styleRule.parentStyleSheet?.cssRules || [] - )?.filter(ele => ele.type === CSSRule.STYLE_RULE) as CSSStyleRule[]; - const previousStyleRule = cssStyleRules.find( - ele => ele?.selectorText === selector - ); - if (!previousStyleRule) return false; - const currPosition = Array.from( - styleRule.parentStyleSheet?.cssRules || [] - ).indexOf(styleRule); - const prevPosition = Array.from( - previousStyleRule?.parentStyleSheet?.cssRules || [] - ).indexOf(previousStyleRule); - return currPosition > prevPosition; -}; - -class CSSHelp { - doc: HTMLDocument; - constructor(doc: HTMLDocument) { - this.doc = doc; - } - private _getStyleRules() { - const styleSheet = this.getStyleSheet(); - return this.styleSheetToCssRulesArray(styleSheet).filter( - ele => ele.type === CSSRule.STYLE_RULE - ) as CSSStyleRule[]; - } - - getStyleDeclarations(selector: string): CSSStyleDeclaration[] { - return this._getStyleRules() - ?.filter(ele => ele?.selectorText === selector) - .map(x => x.style); - } - getStyle(selector: string): ExtendedStyleDeclaration | null { - const style = this._getStyleRules().find( - ele => ele?.selectorText === selector - )?.style as ExtendedStyleDeclaration | undefined; - if (!style) return null; - style.getPropVal = (prop: string, strip = false) => { - return strip - ? style.getPropertyValue(prop).replace(/\s+/g, '') - : style.getPropertyValue(prop); - }; - return style; - } - getStyleRule(selector: string): ExtendedStyleRule | null { - const styleRule = this._getStyleRules()?.find( - ele => ele?.selectorText === selector - ); - if (styleRule) { - return { - ...styleRule, - isDeclaredAfter: (selector: string) => - getIsDeclaredAfter(styleRule)(selector) - }; - } else { - return null; - } - } - getCSSRules(element?: string): CSSRule[] { - const styleSheet = this.getStyleSheet(); - const cssRules = this.styleSheetToCssRulesArray(styleSheet); - switch (element) { - case 'media': - return cssRules.filter(ele => ele.type === CSSRule.MEDIA_RULE); - case 'fontface': - return cssRules.filter(ele => ele.type === CSSRule.FONT_FACE_RULE); - case 'import': - return cssRules.filter(ele => ele.type === CSSRule.IMPORT_RULE); - case 'keyframes': - return cssRules.filter(ele => ele.type === CSSRule.KEYFRAMES_RULE); - default: - return cssRules; - } - } - isPropertyUsed(property: string): boolean { - return this._getStyleRules().some(ele => - ele.style?.getPropertyValue(property) - ); - } - getRuleListsWithinMedia(mediaText: string): CSSStyleRule[] { - const medias = this.getCSSRules('media') as CSSMediaRule[]; - const cond = medias?.find(x => x?.media?.mediaText === mediaText); - const cssRules = cond?.cssRules; - return Array.from(cssRules || []) as CSSStyleRule[]; - } - getStyleSheet(): CSSStyleSheet | null { - // TODO: Change selector to match exactly 'styles.css' - const link: HTMLLinkElement | null = this.doc?.querySelector( - "link[href*='styles']" - ); - - // When using the styles.css tab, we add a 'fcc-injected-styles' class so we can target that. This allows users to add external scripts without them interfering - const stylesDotCss: HTMLStyleElement | null = this.doc?.querySelector( - 'style.fcc-injected-styles' - ); - - // For steps that use