feat(guide): Import guide in to the client app
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
const { getChallenges } = require('@freecodecamp/curriculum');
|
||||
const { getChallengesForLang } = require('@freecodecamp/curriculum');
|
||||
const { from, of } = require('rxjs');
|
||||
const { map } = require('rxjs/operators');
|
||||
const _ = require('lodash');
|
||||
@@ -11,97 +11,64 @@ const nameify = utils.nameify;
|
||||
const arrToString = arr =>
|
||||
Array.isArray(arr) ? arr.join('\n') : _.toString(arr);
|
||||
|
||||
exports.buildChallenges$ = function buildChallenges$() {
|
||||
return from(getChallenges()).pipe(
|
||||
map(function(challengeSpec) {
|
||||
const order = challengeSpec.order;
|
||||
const blockName = challengeSpec.name;
|
||||
const superBlock = challengeSpec.superBlock;
|
||||
const superOrder = challengeSpec.superOrder;
|
||||
const isBeta = !!challengeSpec.isBeta;
|
||||
const isComingSoon = !!challengeSpec.isComingSoon;
|
||||
const fileName = challengeSpec.fileName;
|
||||
const time = challengeSpec.time;
|
||||
const isLocked = !!challengeSpec.isLocked;
|
||||
const message = challengeSpec.message;
|
||||
const required = challengeSpec.required || [];
|
||||
const template = challengeSpec.template;
|
||||
const isPrivate = !!challengeSpec.isPrivate;
|
||||
exports.buildChallenges = async function buildChallenges() {
|
||||
const curriculum = await getChallengesForLang('english');
|
||||
const superBlocks = Object.keys(curriculum);
|
||||
const blocks = superBlocks
|
||||
.map(superBlock => curriculum[superBlock].blocks)
|
||||
.reduce((blocks, superBlock) => {
|
||||
const currentBlocks = Object.keys(superBlock).map(key => superBlock[key]);
|
||||
return blocks.concat(_.flatten(currentBlocks));
|
||||
}, []);
|
||||
|
||||
// challenge file has no challenges...
|
||||
if (challengeSpec.challenges.length === 0) {
|
||||
return of([{ block: 'empty ' + blockName }]);
|
||||
const builtChallenges = blocks.filter(block => !block.isPrivate).map(({ meta, challenges }) => {
|
||||
const {
|
||||
order,
|
||||
time,
|
||||
template,
|
||||
required,
|
||||
superBlock,
|
||||
superOrder,
|
||||
isPrivate,
|
||||
dashedName: blockDashedName,
|
||||
fileName
|
||||
} = meta;
|
||||
|
||||
return challenges.map(challenge => {
|
||||
challenge.name = nameify(challenge.title);
|
||||
|
||||
challenge.dashedName = dasherize(challenge.name);
|
||||
|
||||
if (challenge.files) {
|
||||
challenge.files = _.reduce(
|
||||
challenge.files,
|
||||
(map, file) => {
|
||||
map[file.key] = {
|
||||
...file,
|
||||
head: arrToString(file.head),
|
||||
contents: arrToString(file.contents),
|
||||
tail: arrToString(file.tail)
|
||||
};
|
||||
return map;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
const block = {
|
||||
title: blockName,
|
||||
name: nameify(blockName),
|
||||
dashedName: dasherize(blockName),
|
||||
superOrder,
|
||||
superBlock,
|
||||
superBlockMessage: message,
|
||||
order,
|
||||
time,
|
||||
isLocked,
|
||||
isPrivate
|
||||
};
|
||||
|
||||
return challengeSpec.challenges.map(function(challenge, index) {
|
||||
challenge.name = nameify(challenge.title);
|
||||
|
||||
challenge.dashedName = dasherize(challenge.name);
|
||||
|
||||
if (challenge.files) {
|
||||
challenge.files = _.reduce(
|
||||
challenge.files,
|
||||
(map, file) => {
|
||||
map[file.key] = {
|
||||
...file,
|
||||
head: arrToString(file.head),
|
||||
contents: arrToString(file.contents),
|
||||
tail: arrToString(file.tail)
|
||||
};
|
||||
return map;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
challenge.fileName = fileName;
|
||||
challenge.order = order;
|
||||
challenge.suborder = index + 1;
|
||||
challenge.block = dasherize(blockName);
|
||||
challenge.blockId = block.id;
|
||||
challenge.isBeta = challenge.isBeta || isBeta;
|
||||
challenge.isComingSoon = challenge.isComingSoon || isComingSoon;
|
||||
challenge.isLocked = challenge.isLocked || isLocked;
|
||||
challenge.isPrivate = challenge.isPrivate || isPrivate;
|
||||
challenge.isRequired = !!challenge.isRequired;
|
||||
challenge.time = challengeSpec.time;
|
||||
challenge.superOrder = superOrder;
|
||||
challenge.superBlock = superBlock
|
||||
.split('-')
|
||||
.map(function(word) {
|
||||
return _.capitalize(word);
|
||||
})
|
||||
.join(' ');
|
||||
challenge.required = (challenge.required || []).concat(required);
|
||||
challenge.template = challenge.template || template;
|
||||
|
||||
return _.omit(challenge, [
|
||||
'betaSolutions',
|
||||
'betaTests',
|
||||
'hints',
|
||||
'MDNlinks',
|
||||
'null',
|
||||
'rawSolutions',
|
||||
'react',
|
||||
'reactRedux',
|
||||
'redux',
|
||||
'releasedOn',
|
||||
'translations',
|
||||
'type'
|
||||
]);
|
||||
});
|
||||
})
|
||||
);
|
||||
challenge.fileName = fileName;
|
||||
challenge.order = order;
|
||||
challenge.block = blockDashedName;
|
||||
challenge.isPrivate = challenge.isPrivate || isPrivate;
|
||||
challenge.isRequired = !!challenge.isRequired;
|
||||
challenge.time = time;
|
||||
challenge.superOrder = superOrder;
|
||||
challenge.superBlock = superBlock
|
||||
.split('-')
|
||||
.map(word => _.capitalize(word))
|
||||
.join(' ');
|
||||
challenge.required = required;
|
||||
challenge.template = template;
|
||||
return challenge;
|
||||
});
|
||||
}).reduce((accu, current) => accu.concat(current), [])
|
||||
return builtChallenges;
|
||||
};
|
||||
|
177
client/utils/formatting.js
Normal file
177
client/utils/formatting.js
Normal file
@@ -0,0 +1,177 @@
|
||||
/* eslint-disable camelcase */
|
||||
const preFormatted = {
|
||||
acid: 'ACID',
|
||||
arraybuffer: 'ArrayBuffer',
|
||||
aws: 'AWS',
|
||||
bytelength: 'byteLength',
|
||||
charat: 'charAt',
|
||||
charcodeat: 'charCodeAt',
|
||||
codepointat: 'codePointAt',
|
||||
columnnumber: 'columnNumber',
|
||||
compareexchange: 'compareExchange',
|
||||
copywithin: 'copyWithin',
|
||||
cplusplus: 'C++',
|
||||
css: 'CSS',
|
||||
css3: 'CSS3',
|
||||
defineproperties: 'defineProperties',
|
||||
defineproperty: 'defineProperty',
|
||||
deleteproperty: 'deleteProperty',
|
||||
displayname: 'displayName',
|
||||
dynamodb: 'DynamoDB',
|
||||
e: 'E',
|
||||
endswith: 'endsWith',
|
||||
epsilon: 'EPSILON',
|
||||
filename: 'fileName',
|
||||
findindex: 'findIndex',
|
||||
foreach: 'forEach',
|
||||
fromcharcode: 'fromCharCode',
|
||||
frompointcode: 'fromPointCode',
|
||||
getdate: 'getDate',
|
||||
getday: 'getDay',
|
||||
getfullyear: 'getFullYear',
|
||||
gethours: 'getHours',
|
||||
getmilliseconds: 'getMilliseconds',
|
||||
getminutes: 'getMinutes',
|
||||
getmonth: 'getMonth',
|
||||
getownpropertydescriptor: 'getOwnPropertyDescriptor',
|
||||
getownpropertydescriptors: 'getOwnPropertyDescriptors',
|
||||
getownpropertynames: 'getOwnPropertyNames',
|
||||
getownpropertysymbols: 'getOwnPropertySymbols',
|
||||
getprototypeof: 'getPrototypeOf',
|
||||
getseconds: 'getSeconds',
|
||||
gettime: 'getTime',
|
||||
getomezoneoffset: 'getTimezoneOffset',
|
||||
getutcdate: 'getUTCDate',
|
||||
getutcday: 'getUTCDay',
|
||||
getutcfullyear: 'getUTCFullYear',
|
||||
getutchours: 'getUTCHours',
|
||||
getutcmilliseconds: 'getUTCMilliseconds',
|
||||
getutcminutes: 'getUTCMinutes',
|
||||
getutcmonth: 'getUTCMonth',
|
||||
getutcseconds: 'getUTCSeconds',
|
||||
getyear: 'getYear',
|
||||
hasownproperty: 'hasOwnProperty',
|
||||
html: 'HTML',
|
||||
html5: 'HTML5',
|
||||
ide: 'IDE',
|
||||
indexof: 'indexOf',
|
||||
ignorecase: 'ignoreCase',
|
||||
io: 'IO',
|
||||
isarray: 'isArray',
|
||||
isealed: 'isSealed',
|
||||
isextensible: 'isExtensible',
|
||||
isfinite: 'isFinite',
|
||||
isfrozen: 'isFrozen',
|
||||
isgenerator: 'isGenerator',
|
||||
isinteger: 'isInteger',
|
||||
islockfree: 'isLockFree',
|
||||
isnan: 'isNaN',
|
||||
issafeinteger: 'isSafeInteger',
|
||||
isview: 'isView',
|
||||
javascript: 'JavaScript',
|
||||
jquery: 'jQuery',
|
||||
json: 'JSON',
|
||||
lastindexof: 'lastIndexOf',
|
||||
linenumber: 'lineNumber',
|
||||
ln2: 'LN2',
|
||||
ln10: 'LN10',
|
||||
localcompare: 'localCompare',
|
||||
log2e: 'LOG2E',
|
||||
log10e: 'LOG10E',
|
||||
max_safe_integer: 'MAX_SAFE_INTEGER',
|
||||
max_value: 'MAX_VALUE',
|
||||
min_safe_integer: 'MIN_SAFE_INTEGER',
|
||||
min_value: 'MIN_VALUE',
|
||||
mongodb: 'MongoDB',
|
||||
nan: 'NaN',
|
||||
negative_infinity: 'NEGATIVE_INFINITY',
|
||||
padend: 'padEnd',
|
||||
padstart: 'padStart',
|
||||
parsefloat: 'parseFloat',
|
||||
parseint: 'parseInt',
|
||||
pi: 'PI',
|
||||
positive_infinity: 'POSITIVE_INFINITY',
|
||||
preventextentions: 'preventExtensions',
|
||||
propertyisenumerable: 'propertyIsEnumerable',
|
||||
reduceright: 'reduceRight',
|
||||
regexp: 'RegExp',
|
||||
setdate: 'setDate',
|
||||
setfullyear: 'setFullYear',
|
||||
sethours: 'setHours',
|
||||
setmilliseconds: 'setMilliseconds',
|
||||
setminutes: 'setMinutes',
|
||||
setmonth: 'setMonth',
|
||||
setprototypeof: 'setPrototypeOf',
|
||||
setseconds: 'setSeconds',
|
||||
settime: 'setTime',
|
||||
setutcdate: 'setUTCDate',
|
||||
setutcfullyear: 'setUTCFullYear',
|
||||
setutchours: 'setUTCHours',
|
||||
setutcmilliseconds: 'setUTCMilliseconds',
|
||||
setutcminutes: 'setUTCMinutes',
|
||||
setutcmonth: 'setUTCMonth',
|
||||
setutcseconds: 'setUTCSeconds',
|
||||
setyear: 'setYear',
|
||||
sql: 'SQL',
|
||||
sqrt1_2: 'SQRT1_2',
|
||||
sqrt2: 'SQRT2',
|
||||
startswith: 'startsWith',
|
||||
todatestring: 'toDateString',
|
||||
toexponential: 'toExponential',
|
||||
tofixed: 'toFixed',
|
||||
toisostring: 'toISOString',
|
||||
tojson: 'toJSON',
|
||||
tolocaledatestring: 'toLocaleDateString',
|
||||
tolocalelowercase: 'toLocaleLowerCase',
|
||||
tolocalestring: 'toLocaleString',
|
||||
tolocaletimestring: 'toLocaleTimeString',
|
||||
tolocaleuppercase: 'toLocaleUpperCase',
|
||||
tolowercase: 'toLowerCase',
|
||||
toprecision: 'toPrecision',
|
||||
tosource: 'toSource',
|
||||
tostring: 'toString',
|
||||
totimestring: 'toTimeString',
|
||||
touppercase: 'toUpperCase',
|
||||
toutcstring: 'toUTCString',
|
||||
trimleft: 'trimLeft',
|
||||
trimRight: 'trimRight',
|
||||
valueof: 'valueOf',
|
||||
__definegetter__: '__defineGetter__',
|
||||
__definesetter__: '__defineSetter__',
|
||||
__lookupgetter__: '__lookupGetter__',
|
||||
__lookupsetter__: '__lookupSetter__'
|
||||
};
|
||||
|
||||
const stopWords = [
|
||||
'a',
|
||||
'am',
|
||||
'an',
|
||||
'and',
|
||||
'as',
|
||||
'at',
|
||||
'but',
|
||||
'by',
|
||||
'for',
|
||||
'from',
|
||||
'if',
|
||||
'in',
|
||||
'into',
|
||||
'it',
|
||||
'it\'s',
|
||||
'its',
|
||||
'no',
|
||||
'nor',
|
||||
'not',
|
||||
'of',
|
||||
'off',
|
||||
'on',
|
||||
'or',
|
||||
'the',
|
||||
'to',
|
||||
'with'
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
preFormatted,
|
||||
stopWords
|
||||
};
|
@@ -1,6 +1,9 @@
|
||||
const { dasherize } = require('..');
|
||||
|
||||
const path = require('path');
|
||||
const select = require('unist-util-select');
|
||||
const { head } = require('lodash');
|
||||
|
||||
const { dasherize } = require('..');
|
||||
const { isAStubRE } = require('../regEx');
|
||||
|
||||
const { viewTypes } = require('../challengeTypes');
|
||||
|
||||
@@ -25,6 +28,11 @@ const superBlockIntro = path.resolve(
|
||||
'../../src/templates/Introduction/SuperBlockIntro.js'
|
||||
);
|
||||
|
||||
const guideArticle = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Guide/GuideArticle.js'
|
||||
);
|
||||
|
||||
const views = {
|
||||
backend,
|
||||
classic,
|
||||
@@ -41,7 +49,7 @@ const getTemplateComponent = challengeType => views[viewTypes[challengeType]];
|
||||
|
||||
const getIntroIfRequired = (node, index, nodeArray) => {
|
||||
const next = nodeArray[index + 1];
|
||||
const isEndOfBlock = next && next.node.suborder === 1;
|
||||
const isEndOfBlock = next && next.node.challengeOrder === 1;
|
||||
let nextSuperBlock = '';
|
||||
let nextBlock = '';
|
||||
if (next) {
|
||||
@@ -55,7 +63,13 @@ const getIntroIfRequired = (node, index, nodeArray) => {
|
||||
};
|
||||
|
||||
exports.createChallengePages = createPage => ({ node }, index, thisArray) => {
|
||||
const { fields: { slug }, required = [], template, challengeType, id } = node;
|
||||
const {
|
||||
fields: { slug },
|
||||
required = [],
|
||||
template,
|
||||
challengeType,
|
||||
id
|
||||
} = node;
|
||||
if (challengeType === 7) {
|
||||
return;
|
||||
}
|
||||
@@ -76,28 +90,65 @@ exports.createChallengePages = createPage => ({ node }, index, thisArray) => {
|
||||
});
|
||||
};
|
||||
|
||||
exports.createIntroPages = createPage => edge => {
|
||||
const { fields: { slug }, frontmatter: { superBlock, block } } = edge.node;
|
||||
exports.createBlockIntroPages = createPage => edge => {
|
||||
const {
|
||||
fields: { slug },
|
||||
frontmatter: { block }
|
||||
} = edge.node;
|
||||
|
||||
// If there is no block specified in the markdown we assume the markdown is
|
||||
// for a superblock introduction. Otherwise create a block intro page.
|
||||
if (!block) {
|
||||
createPage({
|
||||
path: slug,
|
||||
component: superBlockIntro,
|
||||
context: {
|
||||
superBlock: dasherize(superBlock),
|
||||
slug
|
||||
}
|
||||
});
|
||||
} else {
|
||||
createPage({
|
||||
path: slug,
|
||||
component: intro,
|
||||
context: {
|
||||
block: dasherize(block),
|
||||
slug
|
||||
}
|
||||
});
|
||||
}
|
||||
createPage({
|
||||
path: slug,
|
||||
component: intro,
|
||||
context: {
|
||||
block: dasherize(block),
|
||||
slug
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
exports.createSuperBlockIntroPages = createPage => edge => {
|
||||
const {
|
||||
fields: { slug },
|
||||
frontmatter: { superBlock }
|
||||
} = edge.node;
|
||||
|
||||
createPage({
|
||||
path: slug,
|
||||
component: superBlockIntro,
|
||||
context: {
|
||||
superBlock: dasherize(superBlock),
|
||||
slug
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
exports.createGuideArticlePages = createPage => ({
|
||||
node: {
|
||||
htmlAst,
|
||||
excerpt,
|
||||
fields: { slug },
|
||||
id
|
||||
}
|
||||
}) => {
|
||||
let meta = {};
|
||||
|
||||
if (!isAStubRE.test(excerpt)) {
|
||||
const featureImage = head(select(htmlAst, 'element[tagName=img]'));
|
||||
meta.featureImage = featureImage
|
||||
? featureImage.properties.src
|
||||
: 'https://s3.amazonaws.com/freecodecamp' +
|
||||
'/reecodecamp-square-logo-large.jpg';
|
||||
|
||||
const description = head(select(htmlAst, 'element[tagName=p]'));
|
||||
meta.description = description ? description.children[0].value : '';
|
||||
}
|
||||
|
||||
createPage({
|
||||
path: slug,
|
||||
component: guideArticle,
|
||||
context: {
|
||||
id,
|
||||
meta
|
||||
}
|
||||
});
|
||||
};
|
||||
|
5
client/utils/infoLog.js
Normal file
5
client/utils/infoLog.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const chalk = require('chalk');
|
||||
|
||||
module.exports = function info(str, colour = 'red') {
|
||||
console.info(chalk[colour](str));
|
||||
};
|
10
client/utils/readDir.js
Normal file
10
client/utils/readDir.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const fse = require('fs-extra');
|
||||
|
||||
const { isAFileRE, shouldBeIgnoredRE } = require('./regEx.js');
|
||||
|
||||
module.exports = function readDir(dir) {
|
||||
return fse
|
||||
.readdirSync(dir)
|
||||
.filter(item => !isAFileRE.test(item))
|
||||
.filter(file => !shouldBeIgnoredRE.test(file));
|
||||
};
|
6
client/utils/regEx.js
Normal file
6
client/utils/regEx.js
Normal file
@@ -0,0 +1,6 @@
|
||||
exports.httpsRE = /https?\:\/\//;
|
||||
exports.isAFileRE = /(\.md|\.jsx?|\.html?)$/;
|
||||
exports.isAStubRE = /This\sis\sa\sstub.+?Help\sour\scommunity\sexpand\sit/;
|
||||
exports.markdownLinkRE = /\!?\[.*?\]\(.+?\)/g;
|
||||
exports.metaTitleRE = /^---\r?\ntitle:([^\n]*)\n---$/m;
|
||||
exports.shouldBeIgnoredRE = /^(\_|\.)/;
|
72
client/utils/titleify.js
Normal file
72
client/utils/titleify.js
Normal file
@@ -0,0 +1,72 @@
|
||||
const { preFormatted, stopWords } = require('./formatting');
|
||||
|
||||
const prototypeRE = /prototype/i;
|
||||
const prototypesRE = /prototypes/i;
|
||||
|
||||
const removeProto = x => x !== 'prototype';
|
||||
|
||||
function titleCase(word) {
|
||||
return word[0].toUpperCase() + word.slice(1);
|
||||
}
|
||||
|
||||
function prototyper(str) {
|
||||
const formatted = str
|
||||
.replace(/javascript/i, '')
|
||||
.split('-')
|
||||
.map(str => {
|
||||
if (prototypeRE.test(str)) {
|
||||
if (str.length > 9) {
|
||||
return prototyper(
|
||||
str.trim().split('prototype').join('-prototype-')
|
||||
);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
return titleify(str);
|
||||
})
|
||||
.join(' ')
|
||||
.split(' ');
|
||||
const noProto = formatted
|
||||
.filter(removeProto)
|
||||
.filter(x => !!x);
|
||||
if (noProto.length === 2) {
|
||||
const [ first, second ] = noProto;
|
||||
const secondLC = second.toLowerCase();
|
||||
const finalSecond = preFormatted[secondLC] ?
|
||||
preFormatted[secondLC] :
|
||||
secondLC;
|
||||
return `${titleify(first)}.prototype.${finalSecond}`;
|
||||
}
|
||||
if (noProto.length === 1) {
|
||||
return prototyper(
|
||||
noProto[0]
|
||||
.toLowerCase()
|
||||
.split('.')
|
||||
.join('-')
|
||||
);
|
||||
}
|
||||
return titleify(str, true);
|
||||
}
|
||||
|
||||
function titleify(str, triedPrototyper) {
|
||||
if (str.match(prototypeRE) && !triedPrototyper && !prototypesRE.test(str)) {
|
||||
return prototyper(str);
|
||||
}
|
||||
return str
|
||||
.toLowerCase()
|
||||
.split('-')
|
||||
.map((word, i) => {
|
||||
if (!word) {
|
||||
return '';
|
||||
}
|
||||
if (stopWords.some(x => x === word) && i !== 0) {
|
||||
return word;
|
||||
}
|
||||
return preFormatted[word] ?
|
||||
preFormatted[word] :
|
||||
titleCase(word);
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
module.exports = titleify;
|
Reference in New Issue
Block a user