mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-09-24 23:01:18 +08:00
chore(client): make curriculum helpers external (#46906)
* chore: make curriculum helpers external * feat: add curriculum helpers as a package * fix: use right preset? * fix: update version with babel transpiled files * fix: re-add fixture * fix: declare as module * fix: add declaration files in curriculum helpers * fix: nothing with version 1.0.4 * fix: missing functions in package * fix: remove old babel * fix: linting
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -1,30 +0,0 @@
|
||||
const htmlFullExample = `
|
||||
<!--
|
||||
multi line html comment
|
||||
-->
|
||||
|
||||
not a comment
|
||||
|
||||
not a commment <!-- single line html comment --> not a comment
|
||||
not a comment
|
||||
<!-- this is my blog: <mynixworld.inf> -->
|
||||
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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 = '';
|
||||
});
|
||||
});
|
||||
@@ -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 <style> tags, where they don't add the above class - most* browser extensions inject styles with class/media attributes, so it filters those
|
||||
const styleTag: HTMLStyleElement | null = this.doc?.querySelector(
|
||||
'style:not([class]):not([media])'
|
||||
);
|
||||
|
||||
if (link?.sheet?.cssRules?.length) {
|
||||
return link.sheet;
|
||||
} else if (stylesDotCss) {
|
||||
return stylesDotCss.sheet;
|
||||
} else if (styleTag) {
|
||||
return styleTag.sheet;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
styleSheetToCssRulesArray(
|
||||
styleSheet: ReturnType<CSSHelp['getStyleSheet']>
|
||||
): CSSRule[] {
|
||||
return Array.from(styleSheet?.cssRules || []);
|
||||
}
|
||||
}
|
||||
|
||||
export default CSSHelp;
|
||||
@@ -1,108 +0,0 @@
|
||||
import cssTestValues from './__fixtures/curriculum-helpers-css';
|
||||
import htmlTestValues from './__fixtures/curriculum-helpers-html';
|
||||
import jsTestValues from './__fixtures/curriculum-helpers-javascript';
|
||||
import whiteSpaceTestValues from './__fixtures/curriculum-helpers-remove-white-space';
|
||||
import __testHelpers, { removeJSComments } from './curriculum-helpers';
|
||||
|
||||
const { stringWithWhiteSpaceChars, stringWithWhiteSpaceCharsRemoved } =
|
||||
whiteSpaceTestValues;
|
||||
|
||||
const { cssFullExample, cssCodeWithCommentsRemoved } = cssTestValues;
|
||||
|
||||
const { htmlFullExample, htmlCodeWithCommentsRemoved } = htmlTestValues;
|
||||
|
||||
const {
|
||||
jsCodeWithSingleAndMultLineComments,
|
||||
jsCodeWithSingleAndMultLineCommentsRemoved,
|
||||
jsCodeWithUrl,
|
||||
jsCodeWithUrlUnchanged,
|
||||
jsCodeWithNoCall,
|
||||
jsCodeWithNoArgCall,
|
||||
jsCodeWithArgCall,
|
||||
jsCodeWithCommentedCall
|
||||
} = jsTestValues;
|
||||
|
||||
describe('removeWhiteSpace', () => {
|
||||
const { removeWhiteSpace } = __testHelpers;
|
||||
it('returns a string', () => {
|
||||
expect(typeof removeWhiteSpace('This should return a string')).toBe(
|
||||
'string'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns a string with no white space characters', () => {
|
||||
expect(removeWhiteSpace(stringWithWhiteSpaceChars)).toBe(
|
||||
stringWithWhiteSpaceCharsRemoved
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeJSComments', () => {
|
||||
it('returns a string', () => {
|
||||
expect(typeof removeJSComments('const should = "return a string"')).toBe(
|
||||
'string'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns a string with no single or multi-line comments', () => {
|
||||
expect(removeJSComments(jsCodeWithSingleAndMultLineComments)).toBe(
|
||||
jsCodeWithSingleAndMultLineCommentsRemoved
|
||||
);
|
||||
});
|
||||
|
||||
it('leaves malformed JS unchanged', () => {
|
||||
const actual = '/ unclosed regex';
|
||||
expect(removeJSComments(actual)).toBe(actual);
|
||||
});
|
||||
|
||||
it('does not remove a url found in JS code', () => {
|
||||
expect(removeJSComments(jsCodeWithUrl)).toBe(jsCodeWithUrlUnchanged);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeCssComments', () => {
|
||||
const { removeCssComments } = __testHelpers;
|
||||
it('returns a string', () => {
|
||||
expect(typeof removeCssComments('.aClass: { color: red; }')).toBe('string');
|
||||
});
|
||||
|
||||
it('returns a CSS string with no single or multi-line comments', () => {
|
||||
expect(removeCssComments(cssFullExample)).toBe(cssCodeWithCommentsRemoved);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeHtmlComments', () => {
|
||||
const { removeHtmlComments } = __testHelpers;
|
||||
it('returns a string', () => {
|
||||
expect(
|
||||
typeof removeHtmlComments(
|
||||
'<h1>hello world</h1><!-- a comment--><h2>h2 element</h2>'
|
||||
)
|
||||
).toBe('string');
|
||||
});
|
||||
|
||||
it('returns an HTML string with no single or multi-line comments', () => {
|
||||
expect(removeHtmlComments(htmlFullExample)).toBe(
|
||||
htmlCodeWithCommentsRemoved
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCalledWithNoArgs', () => {
|
||||
const { isCalledWithNoArgs } = __testHelpers;
|
||||
it('returns a boolean', () => {
|
||||
expect(typeof isCalledWithNoArgs('foo', 'bar')).toBe('boolean');
|
||||
});
|
||||
it('returns false when not called', () => {
|
||||
expect(isCalledWithNoArgs('myFunc', jsCodeWithNoCall)).toBe(false);
|
||||
});
|
||||
it('returns true for a call with no arguments', () => {
|
||||
expect(isCalledWithNoArgs('myFunc', jsCodeWithNoArgCall)).toBe(true);
|
||||
});
|
||||
it('returns false for a call with arguments', () => {
|
||||
expect(isCalledWithNoArgs('myFunc', jsCodeWithArgCall)).toBe(false);
|
||||
});
|
||||
it('returns false for a commented out call', () => {
|
||||
expect(isCalledWithNoArgs('myFunc', jsCodeWithCommentedCall)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,58 +0,0 @@
|
||||
import strip from '@freecodecamp/strip-comments';
|
||||
import CSSHelp from './css-help';
|
||||
|
||||
const removeHtmlComments = (str: string): string =>
|
||||
str.replace(/<!--[\s\S]*?(-->|$)/g, '');
|
||||
|
||||
const removeCssComments = (str: string): string =>
|
||||
str.replace(/\/\*[\s\S]+?\*\//g, '');
|
||||
|
||||
export const removeJSComments = (codeStr: string): string => {
|
||||
// TODO: publish type declarations and re-enable eslint
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
return strip(codeStr) as string;
|
||||
} catch (err) {
|
||||
return codeStr;
|
||||
}
|
||||
};
|
||||
|
||||
const removeWhiteSpace = (str = ''): string => {
|
||||
return str.replace(/\s/g, '');
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
|
||||
function escapeRegExp(exp: string): string {
|
||||
return exp.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
/*
|
||||
This helper checks if a function/method is called with no arguments.
|
||||
|
||||
Because Safari does not support lookbehinds (as of writing this on
|
||||
July 14 2021), avoiding false matches on function definitions is done by
|
||||
checking that only whitespace characters precede the calling name on the line
|
||||
it is found on. That makes this helper incompatible with
|
||||
removeWhiteSpace() above, which removes all whitespace characters.
|
||||
*/
|
||||
function isCalledWithNoArgs(
|
||||
calledFuncName: string,
|
||||
callingCode: string
|
||||
): boolean {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
const noCommentsCallingCode = strip(callingCode) as string;
|
||||
const funcExp = `^\\s*?${escapeRegExp(calledFuncName)}\\(\\s*?\\)`;
|
||||
const matches = new RegExp(funcExp, 'gm').exec(noCommentsCallingCode) ?? [];
|
||||
|
||||
return !!matches.length;
|
||||
}
|
||||
|
||||
const curriculumHelpers = {
|
||||
removeHtmlComments,
|
||||
removeCssComments,
|
||||
removeWhiteSpace,
|
||||
isCalledWithNoArgs,
|
||||
CSSHelp
|
||||
};
|
||||
|
||||
export default curriculumHelpers;
|
||||
Generated
+21
@@ -446,6 +446,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",
|
||||
@@ -3372,6 +3373,17 @@
|
||||
"resolved": "tools/challenge-helper-scripts",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@freecodecamp/curriculum-helpers": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@freecodecamp/curriculum-helpers/-/curriculum-helpers-1.0.5.tgz",
|
||||
"integrity": "sha512-GjaJXju1slWKtNb01xY6lbDjhuIQLEcLysdhi8d1eIfnvx94Jaxh1tBvxYiFbeI1xf+oeS+6gAy/GIEa7PP0KQ==",
|
||||
"dependencies": {
|
||||
"@freecodecamp/strip-comments": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@freecodecamp/fcc-source-challenges": {
|
||||
"resolved": "client/plugins/fcc-source-challenges",
|
||||
"link": true
|
||||
@@ -56312,6 +56324,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",
|
||||
@@ -56543,6 +56556,14 @@
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
},
|
||||
"@freecodecamp/curriculum-helpers": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@freecodecamp/curriculum-helpers/-/curriculum-helpers-1.0.5.tgz",
|
||||
"integrity": "sha512-GjaJXju1slWKtNb01xY6lbDjhuIQLEcLysdhi8d1eIfnvx94Jaxh1tBvxYiFbeI1xf+oeS+6gAy/GIEa7PP0KQ==",
|
||||
"requires": {
|
||||
"@freecodecamp/strip-comments": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"@freecodecamp/fcc-source-challenges": {
|
||||
"version": "file:client/plugins/fcc-source-challenges"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user