mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
refactor: create shared workspace (#51454)
This commit is contained in:
committed by
GitHub
parent
3c0c14b427
commit
391fc2e34d
@@ -0,0 +1,33 @@
|
||||
import { getLines } from './get-lines';
|
||||
|
||||
const content = 'one\ntwo\nthree';
|
||||
|
||||
describe('dasherize', () => {
|
||||
it('returns a string', () => {
|
||||
expect(getLines('')).toBe('');
|
||||
});
|
||||
it("returns '' when the second arg is empty", () => {
|
||||
expect(getLines(content)).toBe('');
|
||||
});
|
||||
it("returns '' when the range is negative", () => {
|
||||
expect(getLines(content, [1, -1])).toBe('');
|
||||
});
|
||||
it("returns '' when the range is [n,n]", () => {
|
||||
expect(getLines(content, [0, 0])).toBe('');
|
||||
expect(getLines(content, [1, 1])).toBe('');
|
||||
expect(getLines(content, [2, 2])).toBe('');
|
||||
});
|
||||
|
||||
it('returns the first line when the range is [0,2]', () => {
|
||||
expect(getLines(content, [0, 2])).toBe('one');
|
||||
});
|
||||
it('returns the second line when the range is [1,3]', () => {
|
||||
expect(getLines(content, [1, 3])).toBe('two');
|
||||
});
|
||||
it('returns the first and second lines when the range is [0,3]', () => {
|
||||
expect(getLines(content, [0, 3])).toBe('one\ntwo');
|
||||
});
|
||||
it('returns the second and third lines when the range is [1,4]', () => {
|
||||
expect(getLines(content, [1, 4])).toBe('two\nthree');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
export function getLines(contents: string, range?: number[]): string {
|
||||
if (!range) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const lines = contents.split('\n');
|
||||
const editableLines =
|
||||
range[1] <= range[0] ? [] : lines.slice(range[0], range[1] - 1);
|
||||
return editableLines.join('\n');
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
type SuperBlocks,
|
||||
getAuditedSuperBlocks
|
||||
} from '../../shared/config/superblocks';
|
||||
|
||||
export function isAuditedSuperBlock(
|
||||
language: string,
|
||||
superblock: SuperBlocks,
|
||||
{
|
||||
showNewCurriculum,
|
||||
showUpcomingChanges
|
||||
}: { showNewCurriculum: boolean; showUpcomingChanges: boolean }
|
||||
) {
|
||||
// TODO: when all the consumers of this function use TypeScript we can remove
|
||||
// this check
|
||||
if (!language || !superblock)
|
||||
throw Error('Both arguments must be provided for auditing');
|
||||
|
||||
const auditedSuperBlocks = getAuditedSuperBlocks({
|
||||
showNewCurriculum,
|
||||
showUpcomingChanges,
|
||||
language
|
||||
});
|
||||
return auditedSuperBlocks.includes(superblock);
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// originally based off of https://github.com/gulpjs/vinyl
|
||||
const invariant = require('invariant');
|
||||
|
||||
// interface PolyVinyl {
|
||||
// source: String,
|
||||
// contents: String,
|
||||
// name: String,
|
||||
// ext: String,
|
||||
// path: String,
|
||||
// key: String,
|
||||
// head: String,
|
||||
// tail: String,
|
||||
// history: [...String],
|
||||
// error: Null|Object|Error
|
||||
// }
|
||||
|
||||
// createPoly({
|
||||
// name: String,
|
||||
// ext: String,
|
||||
// contents: String,
|
||||
// history?: [...String],
|
||||
// }) => PolyVinyl, throws
|
||||
function createPoly({ name, ext, contents, history, ...rest } = {}) {
|
||||
invariant(typeof name === 'string', 'name must be a string but got %s', name);
|
||||
|
||||
invariant(typeof ext === 'string', 'ext must be a string, but was %s', ext);
|
||||
|
||||
invariant(
|
||||
typeof contents === 'string',
|
||||
'contents must be a string but got %s',
|
||||
contents
|
||||
);
|
||||
|
||||
return {
|
||||
...rest,
|
||||
history: Array.isArray(history) ? history : [name + '.' + ext],
|
||||
name,
|
||||
ext,
|
||||
path: name + '.' + ext,
|
||||
fileKey: name + ext,
|
||||
contents,
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
// isPoly(poly: Any) => Boolean
|
||||
function isPoly(poly) {
|
||||
return (
|
||||
poly &&
|
||||
typeof poly.contents === 'string' &&
|
||||
typeof poly.name === 'string' &&
|
||||
typeof poly.ext === 'string' &&
|
||||
Array.isArray(poly.history)
|
||||
);
|
||||
}
|
||||
|
||||
// checkPoly(poly: Any) => Void, throws
|
||||
function checkPoly(poly) {
|
||||
invariant(
|
||||
isPoly(poly),
|
||||
'function should receive a PolyVinyl, but got %s',
|
||||
poly
|
||||
);
|
||||
}
|
||||
|
||||
// setContent(contents: String, poly: PolyVinyl) => PolyVinyl
|
||||
// setContent will loose source if set
|
||||
function setContent(contents, poly) {
|
||||
checkPoly(poly);
|
||||
return {
|
||||
...poly,
|
||||
contents,
|
||||
source: null
|
||||
};
|
||||
}
|
||||
|
||||
// setExt(ext: String, poly: PolyVinyl) => PolyVinyl
|
||||
function setExt(ext, poly) {
|
||||
checkPoly(poly);
|
||||
const newPoly = {
|
||||
...poly,
|
||||
ext,
|
||||
path: poly.name + '.' + ext,
|
||||
fileKey: poly.name + ext
|
||||
};
|
||||
newPoly.history = [...poly.history, newPoly.path];
|
||||
return newPoly;
|
||||
}
|
||||
|
||||
// setImportedFiles(importedFiles: String[], poly: PolyVinyl) => PolyVinyl
|
||||
function setImportedFiles(importedFiles, poly) {
|
||||
checkPoly(poly);
|
||||
const newPoly = {
|
||||
...poly,
|
||||
importedFiles: [...importedFiles]
|
||||
};
|
||||
return newPoly;
|
||||
}
|
||||
|
||||
// This is currently only used to add back properties that are not stored in the
|
||||
// database.
|
||||
function regeneratePathAndHistory(poly) {
|
||||
const newPath = poly.name + '.' + poly.ext;
|
||||
const newPoly = {
|
||||
...poly,
|
||||
path: newPath,
|
||||
history: [newPath]
|
||||
};
|
||||
checkPoly(newPoly);
|
||||
return newPoly;
|
||||
}
|
||||
|
||||
// clearHeadTail(poly: PolyVinyl) => PolyVinyl
|
||||
function clearHeadTail(poly) {
|
||||
checkPoly(poly);
|
||||
return {
|
||||
...poly,
|
||||
head: '',
|
||||
tail: ''
|
||||
};
|
||||
}
|
||||
|
||||
// compileHeadTail(padding: String, poly: PolyVinyl) => PolyVinyl
|
||||
function compileHeadTail(padding = '', poly) {
|
||||
return clearHeadTail(
|
||||
transformContents(
|
||||
() => [poly.head, poly.contents, poly.tail].join(padding),
|
||||
poly
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// transformContents(
|
||||
// wrap: (contents: String) => String,
|
||||
// poly: PolyVinyl
|
||||
// ) => PolyVinyl
|
||||
// transformContents will keep a copy of the original
|
||||
// code in the `source` property. If the original polyvinyl
|
||||
// already contains a source, this version will continue as
|
||||
// the source property
|
||||
function transformContents(wrap, poly) {
|
||||
const newPoly = setContent(wrap(poly.contents), poly);
|
||||
// if no source exist, set the original contents as source
|
||||
newPoly.source = poly.source || poly.contents;
|
||||
return newPoly;
|
||||
}
|
||||
|
||||
// transformHeadTailAndContents(
|
||||
// wrap: (source: String) => String,
|
||||
// poly: PolyVinyl
|
||||
// ) => PolyVinyl
|
||||
function transformHeadTailAndContents(wrap, poly) {
|
||||
return {
|
||||
...transformContents(wrap, poly),
|
||||
head: wrap(poly.head),
|
||||
tail: wrap(poly.tail)
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createPoly,
|
||||
isPoly,
|
||||
setContent,
|
||||
setExt,
|
||||
setImportedFiles,
|
||||
compileHeadTail,
|
||||
regeneratePathAndHistory,
|
||||
transformContents,
|
||||
transformHeadTailAndContents
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
isValidUsername,
|
||||
usernameTooShort,
|
||||
validationSuccess,
|
||||
usernameIsHttpStatusCode,
|
||||
invalidCharError,
|
||||
isMicrosoftTranscriptLink
|
||||
} from './validate';
|
||||
|
||||
function inRange(num: number, range: number[]) {
|
||||
return num >= range[0] && num <= range[1];
|
||||
}
|
||||
|
||||
describe('isValidUsername', () => {
|
||||
it('rejects strings with less than 3 characters', () => {
|
||||
expect(isValidUsername('')).toStrictEqual(usernameTooShort);
|
||||
expect(isValidUsername('12')).toStrictEqual(usernameTooShort);
|
||||
expect(isValidUsername('a')).toStrictEqual(usernameTooShort);
|
||||
expect(isValidUsername('12a')).toStrictEqual(validationSuccess);
|
||||
});
|
||||
it('rejects strings which are http response status codes 100-599', () => {
|
||||
expect(isValidUsername('429')).toStrictEqual(usernameIsHttpStatusCode);
|
||||
expect(isValidUsername('789')).toStrictEqual(validationSuccess);
|
||||
});
|
||||
it('rejects non-ASCII characters', () => {
|
||||
expect(isValidUsername('👀👂👄')).toStrictEqual(invalidCharError);
|
||||
});
|
||||
it('rejects with invalidCharError even if the string is too short', () => {
|
||||
expect(isValidUsername('.')).toStrictEqual(invalidCharError);
|
||||
});
|
||||
it('accepts alphanumeric characters', () => {
|
||||
expect(
|
||||
isValidUsername('abcdefghijklmnopqrstuvwxyz0123456789')
|
||||
).toStrictEqual(validationSuccess);
|
||||
});
|
||||
it('accepts hyphens and underscores', () => {
|
||||
expect(isValidUsername('a-b')).toStrictEqual(validationSuccess);
|
||||
expect(isValidUsername('a_b')).toStrictEqual(validationSuccess);
|
||||
});
|
||||
|
||||
it('rejects all other ASCII characters', () => {
|
||||
const allowedCharactersList = ['-', '_', '+'];
|
||||
const numbers = [48, 57];
|
||||
const upperCase = [65, 90];
|
||||
const lowerCase = [97, 122];
|
||||
const base = 'user';
|
||||
const finalCode = 127;
|
||||
|
||||
for (let code = 0; code <= finalCode; code++) {
|
||||
const char = String.fromCharCode(code);
|
||||
let expected: { valid: boolean; error: null | string } = invalidCharError;
|
||||
if (allowedCharactersList.includes(char)) expected = validationSuccess;
|
||||
if (inRange(code, numbers)) expected = validationSuccess;
|
||||
if (inRange(code, upperCase)) expected = validationSuccess;
|
||||
if (inRange(code, lowerCase)) expected = validationSuccess;
|
||||
expect(isValidUsername(base + char)).toStrictEqual(expected);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const baseUrl = 'https://learn.microsoft.com/';
|
||||
|
||||
describe('isMicrosoftTranscriptLink', () => {
|
||||
it('should reject links to domains other than learn.microsoft.com', () => {
|
||||
{
|
||||
expect(isMicrosoftTranscriptLink('https://lean.microsoft.com')).toBe(
|
||||
false
|
||||
);
|
||||
expect(isMicrosoftTranscriptLink('https://learn.microsft.com')).toBe(
|
||||
false
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject links without a username', () => {
|
||||
expect(isMicrosoftTranscriptLink(`${baseUrl}/en-us/users/`)).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject links without a unique id', () => {
|
||||
expect(
|
||||
isMicrosoftTranscriptLink(`${baseUrl}/en-us/users/moT01/transcript`)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject links with anything after the unique id', () => {
|
||||
expect(
|
||||
isMicrosoftTranscriptLink(
|
||||
`${baseUrl}/en-us/users/moT01/transcript/any-id/more-stuff`
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject the placeholder link', () => {
|
||||
expect(
|
||||
isMicrosoftTranscriptLink(
|
||||
'https://learn.microsoft.com/LOCALE/users/USERNAME/transcript/ID'
|
||||
)
|
||||
).toBe(false);
|
||||
expect(
|
||||
isMicrosoftTranscriptLink(
|
||||
'https://learn.microsoft.com/LOCALE/users/USERNAME/transcript/ID/'
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it.each(['en-us', 'fr-fr', 'lang-country'])(
|
||||
'should accept links with the %s locale',
|
||||
locale => {
|
||||
expect(
|
||||
isMicrosoftTranscriptLink(
|
||||
`https://learn.microsoft.com/${locale}/users/moT01/transcript/any-id`
|
||||
)
|
||||
).toBe(true);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
type Valid = {
|
||||
valid: true;
|
||||
error: null;
|
||||
};
|
||||
|
||||
type Invalid = {
|
||||
valid: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
type Validated = Valid | Invalid;
|
||||
|
||||
export const invalidCharError: Invalid = {
|
||||
valid: false,
|
||||
error: 'contains invalid characters'
|
||||
};
|
||||
export const validationSuccess: Valid = { valid: true, error: null };
|
||||
export const usernameTooShort: Invalid = {
|
||||
valid: false,
|
||||
error: 'is too short'
|
||||
};
|
||||
export const usernameIsHttpStatusCode: Invalid = {
|
||||
valid: false,
|
||||
error: 'is a reserved error code'
|
||||
};
|
||||
|
||||
const validCharsRE = /^[a-zA-Z0-9\-_+]*$/;
|
||||
export const isHttpStatusCode = (str: string): boolean => {
|
||||
const output = parseInt(str, 10);
|
||||
return !isNaN(output) && output >= 100 && output <= 599;
|
||||
};
|
||||
|
||||
export const isValidUsername = (str: string): Validated => {
|
||||
if (!validCharsRE.test(str)) return invalidCharError;
|
||||
if (str.length < 3) return usernameTooShort;
|
||||
if (isHttpStatusCode(str)) return usernameIsHttpStatusCode;
|
||||
return validationSuccess;
|
||||
};
|
||||
|
||||
// link template:
|
||||
// https://learn.microsoft.com/LOCALE/users/USERNAME/transcript/ID
|
||||
export const isMicrosoftTranscriptLink = (value: string): boolean => {
|
||||
let url;
|
||||
try {
|
||||
url = new URL(value);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
const correctDomain = url.hostname === 'learn.microsoft.com';
|
||||
const correctPath = !!url.pathname.match(
|
||||
/^\/[^/]+\/users\/[^/]+\/transcript\/[^/]+$/
|
||||
);
|
||||
const notPlaceholder = !url.pathname.match(
|
||||
'/LOCALE/users/USERNAME/transcript/ID'
|
||||
);
|
||||
return correctDomain && correctPath && notPlaceholder;
|
||||
};
|
||||
Reference in New Issue
Block a user