Feat: Ensure markdown formatting (#34547)

<!-- Please follow this checklist and put an x in each of the boxes, like this: [x]. It will ensure that our team takes your pull request seriously. -->

- [x] I have read [freeCodeCamp's contribution guidelines](https://github.com/freeCodeCamp/freeCodeCamp/blob/master/CONTRIBUTING.md).
- [x] My pull request has a descriptive title (not a vague title like `Update index.md`)
- [x] My pull request targets the `master` branch of freeCodeCamp.

Closes #34535
This commit is contained in:
Stuart Taylor
2018-12-06 11:18:56 +00:00
committed by mrugesh mohapatra
parent 6685494344
commit 2d3c2efa2a
7 changed files with 230 additions and 62 deletions

View File

@ -1,10 +1,11 @@
--- ---
title: Context API title: Context API
localeTitle: Context API
--- ---
# Context API # Context API
Новый Context API был реализован в версии React 16.3. Новый Context API был реализован в версии React 16.3.
Он существовал и раньше, однако находился в бета-версии, и, таким образом, не имел возможности предоставить инструменты для работы с его помощью. Он существовал и раньше, однако находился в бета-версии, и, таким образом, не имел возможности предоставить инструменты для работы с его помощью.
@ -94,7 +95,7 @@ export default () => (
```javascript ```javascript
<p> Its 17:00 ! </p> <p> Its 17:00 ! </p>
``` ```
### Динамическое изменение контекста ### Динамическое изменение контекста
Чтобы изменить значение параметра время (time), которое мы передаем компоненту ShowTime, нам нужно предоставить нашему контексту функцию, которая может использоваться потребителем для обновления значения. Чтобы изменить значение параметра время (time), которое мы передаем компоненту ShowTime, нам нужно предоставить нашему контексту функцию, которая может использоваться потребителем для обновления значения.

29
package-lock.json generated
View File

@ -1984,6 +1984,12 @@
"restore-cursor": "^2.0.0" "restore-cursor": "^2.0.0"
} }
}, },
"cli-spinners": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-1.3.1.tgz",
"integrity": "sha512-1QL4544moEsDVH9T/l6Cemov/37iv1RtoKf7NJ04A60+4MREXNfx/QvavbH6QoGdsD4N4Mwy49cmaINR/o2mdg==",
"dev": true
},
"cli-width": { "cli-width": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
@ -6766,6 +6772,15 @@
"lodash._reinterpolate": "~3.0.0" "lodash._reinterpolate": "~3.0.0"
} }
}, },
"log-symbols": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
"integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==",
"dev": true,
"requires": {
"chalk": "^2.0.1"
}
},
"loose-envify": { "loose-envify": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -7649,6 +7664,20 @@
"wordwrap": "~1.0.0" "wordwrap": "~1.0.0"
} }
}, },
"ora": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ora/-/ora-3.0.0.tgz",
"integrity": "sha512-LBS97LFe2RV6GJmXBi6OKcETKyklHNMV0xw7BtsVn2MlsgsydyZetSCbCANr+PFLmDyv4KV88nn0eCKza665Mg==",
"dev": true,
"requires": {
"chalk": "^2.3.1",
"cli-cursor": "^2.1.0",
"cli-spinners": "^1.1.0",
"log-symbols": "^2.2.0",
"strip-ansi": "^4.0.0",
"wcwidth": "^1.0.1"
}
},
"os-homedir": { "os-homedir": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",

View File

@ -18,7 +18,8 @@
"test:lint": "echo 'Warning: TODO - Define Linting tests.'", "test:lint": "echo 'Warning: TODO - Define Linting tests.'",
"test:client": "cd ./client && npm test && cd ../", "test:client": "cd ./client && npm test && cd ../",
"test:curriculum": "cd ./curriculum && npm test && cd ../", "test:curriculum": "cd ./curriculum && npm test && cd ../",
"test:guide-directories": "node ./tools/scripts/ci/ensure-guide-page-naming.js", "test:challenge-formatting": "node ./tools/scripts/ci/ensure-challenge-formatting.js",
"test:guide-formatting": "node ./tools/scripts/ci/ensure-guide-formatting.js",
"test:server": "cd ./api-server && npm test && cd ../", "test:server": "cd ./api-server && npm test && cd ../",
"test:tools": "jest ./tools" "test:tools": "jest ./tools"
}, },
@ -33,6 +34,7 @@
"lerna": "^3.4.0", "lerna": "^3.4.0",
"lodash": "^4.17.11", "lodash": "^4.17.11",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"ora": "^3.0.0",
"readdirp-walk": "^1.6.0", "readdirp-walk": "^1.6.0",
"shortid": "^2.2.14", "shortid": "^2.2.14",
"slugg": "^1.2.1", "slugg": "^1.2.1",

View File

@ -0,0 +1,77 @@
const readdirp = require('readdirp-walk');
const { has, isEmpty, isNumber } = require('lodash');
const ora = require('ora');
const { parseMarkdown } = require('../../challenge-md-parser');
const { challengeRoot, checkFrontmatter } = require('./md-testing-utils');
const scrimbaUrlRE = /^https:\/\/scrimba\.com\//;
const requiredProps = ['title', 'id', 'challengeType'];
const spinner = ora('Checking challenge markdown formatting').start();
readdirp({ root: challengeRoot, directoryFilter: '!_meta' })
.on('data', file =>
Promise.all([
isChallengeParseable(file),
checkFrontmatter(file, {
validator: challengeFrontmatterValidator(file)
})
]).catch(err => {
console.info(`
the following error occured when testing
${file.fullPath}
`);
console.error(err);
// eslint-disable-next-line no-process-exit
process.exit(1);
})
)
.on('end', () => spinner.stop());
const challengeFrontmatterValidator = file => frontmatter => {
const { fullPath } = file;
const hasRequiredProperties = requiredProps
.map(
prop =>
has(frontmatter, prop) &&
(!isEmpty(frontmatter[prop]) || isNumber(frontmatter[prop]))
)
.every(bool => bool);
if (!hasRequiredProperties) {
console.log(`${fullPath} is missing required frontmatter
${JSON.stringify(frontmatter, null, 2)}
Required properties are: ${JSON.stringify(requiredProps, null, 2)}
`);
}
const { videoUrl } = frontmatter;
let validVideoUrl = false;
if (isEmpty(videoUrl)) {
validVideoUrl = true;
} else {
validVideoUrl = scrimbaUrlRE.test(videoUrl);
if (!validVideoUrl) {
console.log(`
${fullPath} contains an invalid videoUrl
`);
}
}
return hasRequiredProperties && validVideoUrl;
};
function isChallengeParseable(file) {
const { stat, fullPath } = file;
if (!stat.isFile() || (/_meta/).test(fullPath)) {
return Promise.resolve(true);
}
return parseMarkdown(fullPath);
}

View File

@ -0,0 +1,34 @@
const readdirp = require('readdirp-walk');
const { has } = require('lodash');
const ora = require('ora');
const {
guideRoot,
checkGuideFile,
checkFrontmatter,
extractLangFromFileName
} = require('./md-testing-utils');
const spinner = ora('Checking guide markdown formatting').start();
const guideFrontmatterValidator = file => frontmatter => {
const hasLocale =
extractLangFromFileName(file) === 'english'
? true
: has(frontmatter, 'localeTitle');
const hasTitle = has(frontmatter, 'title');
return hasLocale && hasTitle;
};
readdirp({ root: guideRoot })
.on('data', file =>
Promise.all([
checkGuideFile(file),
checkFrontmatter(file, { validator: guideFrontmatterValidator(file) })
]).catch(err => {
console.error(err);
// eslint-disable-next-line no-process-exit
process.exit(1);
})
)
.on('end', () => spinner.stop());

View File

@ -1,10 +1,13 @@
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const readdirp = require('readdirp-walk');
const matter = require('gray-matter'); const matter = require('gray-matter');
const _ = require('lodash');
const pass = true;
const guideRoot = path.resolve(__dirname, '../../../guide'); const guideRoot = path.resolve(__dirname, '../../../guide');
const challengeRoot = path.resolve(__dirname, '../../../curriculum/challenges');
exports.guideRoot = guideRoot;
exports.challengeRoot = challengeRoot;
const allowedLangDirNames = [ const allowedLangDirNames = [
'arabic', 'arabic',
@ -15,7 +18,7 @@ const allowedLangDirNames = [
'spanish' 'spanish'
]; ];
function checkFile(file) { exports.checkGuideFile = function checkGuideFile(file) {
const { stat, depth, name, fullPath } = file; const { stat, depth, name, fullPath } = file;
if (depth === 1) { if (depth === 1) {
if (stat.isFile()) { if (stat.isFile()) {
@ -26,43 +29,10 @@ function checkFile(file) {
} }
} }
if (stat.isDirectory()) { if (stat.isDirectory()) {
return checkDirName(name, fullPath).catch(err => { return checkDirName(name, fullPath);
throw err;
});
} }
return checkFileName(name, fullPath) return checkGuideFileName(name, fullPath);
.then(() => checkFrontmatter(fullPath)) };
.catch(err => {
console.log(`
The below occured in:
${fullPath}
`);
console.error(err);
// eslint-disable-next-line no-process-exit
process.exit(1);
});
}
readdirp({ root: guideRoot })
.on('data', file =>
checkFile(file).catch(err => {
console.error(err);
// eslint-disable-next-line no-process-exit
process.exit(1);
})
)
.on('end', () => {
console.log(`
guide directory naming checks complete
`);
// eslint-disable-next-line no-process-exit
process.exit(0);
});
function checkDirName(dirName, fullPath) { function checkDirName(dirName, fullPath) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -86,11 +56,11 @@ Upper case characters found in ${dirName}, all folder names must be lower case
`) `)
); );
} }
return resolve(); return resolve(pass);
}); });
} }
function checkFileName(fileName, fullPath) { function checkGuideFileName(fileName, fullPath) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (fileName !== 'index.md') { if (fileName !== 'index.md') {
return reject( return reject(
@ -103,11 +73,21 @@ function checkFileName(fileName, fullPath) {
) )
); );
} }
return resolve(); return resolve(pass);
}); });
} }
function checkFrontmatter(fullPath) { exports.checkFrontmatter = function checkFrontmatter(
{ fullPath, stat },
options = {
validator() {
return true;
}
}
) {
if (!stat.isFile()) {
return Promise.resolve(pass);
}
return new Promise((resolve, reject) => return new Promise((resolve, reject) =>
fs.readFile(fullPath, 'utf8', (err, content) => { fs.readFile(fullPath, 'utf8', (err, content) => {
if (err) { if (err) {
@ -115,25 +95,15 @@ function checkFrontmatter(fullPath) {
} }
try { try {
const { data: frontmatter } = matter(content); const { data: frontmatter } = matter(content);
if (!frontmatter || _.isEmpty(frontmatter) || !frontmatter.title) { const { validator } = options;
if (!validator(frontmatter)) {
return reject( return reject(
new Error(` new Error(
The article at: ${fullPath} is missing frontmatter. `The article at: ${fullPath} failed frontmatter validation.`
)
Example:
---
title: The Article Title
localeTitle: The Translated Title # Only required for translations
---
< The Article Body >
`)
); );
// process.exit(1);
} }
return resolve(); return resolve(pass);
} catch (e) { } catch (e) {
console.log(` console.log(`
@ -146,4 +116,10 @@ function checkFrontmatter(fullPath) {
} }
}) })
); );
};
function extractLangFromFileName({ path: relativePath }) {
return relativePath.split(path.sep)[0];
} }
exports.extractLangFromFileName = extractLangFromFileName;

View File

@ -0,0 +1,49 @@
/* eslint-disable */
console.log(`
,@@@@@@@@@@,,@@@@@@@% .#&@@@&&.,@@@@@@@@@@, .#&@@@&&. %@@@@@@%* ,@@@% .#&@@@&&. *&@@@@&( ,@@@@@@@% %@@@@@, ,@@,
,@@, ,@@, ,@@/ ./. ,@@, ,@@/ ./. %@% ,&@# .&@&@@( .@@/ ./. #@&. .,/ ,@@, %@% *&@&. ,@@,
,@@, ,@@&%%%%. .&@@/, ,@@, .&@@/, %@% ,&@# %@& /@@, .&@@/, (@@&%(*. ,@@&%%%%. %@% &@# ,@@,
,@@, ,@@/,,,, ./#&@@@( ,@@, ./#&@@@( %@@@@@@%* /@@, #@&. ./#&@@@( *(%&@@&. ,@@/,,,, %@% &@# .&&.
,@@, ,@@, ./, .&@# ,@@, ./, .&@# %@% ,@@@@@@@@@% ./. .&@# /*. /@@. ,@@, %@% *&@&. ,,
,@@, ,@@@@@@@% .#&@@@@&/ ,@@, .#&@@@@&/ %@% .&@# ,@@/.#&@@@@&/ /%&@@@@. ,@@@@@@@% %@@@@@. ,@@,
,*************,,*/(((((//,,*(#%%%%%%%%%%%%%%%#(*,,,****************************************************,*/(((((((((/((((////****/((##%%%%%%
,*************,,//((((((//,,*(%%%%%%%%%%%%%%%%%##/*****************************************************,,*/(///(//////****//((##%%%%%%%%%%%
,************,,*/(((((((//***/#%%%%%%%%%%%%%%%%%%%#(/***************************************************,*//////////*//((#%%%%%%%%%%%%%%%%%
,***********,,*////////////***/##%%%%%%%%%%%%%%%%%%%##(*,***********************************************,,*////////(###%%%%%%%%%%%%%%%%%%%%
,**********,,,*/*******//////**/(#%%%%%%%%%%%%%%%%%%%%%#(/**********************************************,,,***/(##%%%%%%%%%%%%%%%%%%%%%%%%%
,*********,,,,*************///***/(#%%%%%%%%%%%%%%%%%%%%%%#(/***********************************,****,****/((#%%%%%%%%%%%%%%%%%%%%%%%%%%%%#
,*********,,,***************//****/(##%%%%%%%%%%%%%%%%%%%%%%##//**************//////////////////////((#####%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#(
,********,,,,***********************/(#%%%%%%%%%%%%%%%%%%%%%%%##################%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%##(/
,*******,..,***********************,,*/##%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%###((//
,*******,.,,***********************,,,,*(#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%##(//**//
,******,.,,,************************,,,,*/(#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#(//*******
,*****,,,,,********,***,,,,,,,,,,,,*,,,,,,*/(######%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%##(/**********
,*****,..,*******,,,,,,,,,,,,,,,,,,,,,,*,,,,*///((#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%###(/************
,*****,,,*******,,,,,*,,,,,,,,,,,,,,,,,****,,,*/(#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#######(//**************
,****,.,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,**,,,/(%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#((//******************
,***,..,,,,,,,,,,,,,,,,,,,,,,,,,,,,,..,,,,,,,*(#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#(/*******************
,**,,.,,,,,,,,,,,,,,,,,,,,,,,,,,.......,,,,,,/#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#####%%%%%%%%%%%%%%%%#(/******************
,**,..,,,,,,,,,,,,,,,,,,,,,,,,,......,,,*,,,*(#%%%%%%%%##(((/(##%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%##(((/*/((#%%%%%%%%%%%%%%#(/*****************
,*,..,,,,,,,,,,,,,,,,,,,,,,,,,,,.....,,**,,*/#%%%%%%%##((((*,**/#%%%%%%%%%%%%%%%%%%%%%%%%%%%%##((##/,,,*(#%%%%%%%%%%%%%%#(*****************
.*,.,,,**,,,,,,,,,,,,,,,,,,,,,,,,,,*****,,,/(%%%%%%%%#(//(#/,..*/#%%%%%%%%%%%%%%%%%%%%%%%%%%%#(//(#/,..,/(#%%%%%%%%%%%%%%#/*****///////////
.,..,,,,,,,,,,,,,,,,,,,,,,,,,,*,,*******,,,(#%%%%%%%%#(*,,,....,/#%%%%%%%%%%%%%%%%%%%%%%%%%%%#(*,,,....,/(#%%%%%%%%%%%%%%#(*,**////////////
.,..,,,,,,,,,...........,,,,,,*,********,,*(#%%%%%%%%%#(/*,,...,/#%%%%%%%%%%%%%%%%%%%%%%%%%%%%#(/*,,..,*/##%%%%%%%%%%%%%%%#(***////////////
...,,,,,,,................,,*,**********,,/#%%%%%%%%%%%%#((////((#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%##((///(#%%%%%%%%%%%%%%%%%%(/**////////////
..,,,,,,.................,,,**********,,*(#%%%%%%%%%%%%%%%%%%#%%%%%%%%#((///((#%%%%%%%%%%%%%%%%%%%%%#%%%%%%%%%%%%%%%%%%%%%#/**////////////
.,,,,,,,,.................,,***********,,/(####%%%%%%%%%%%%%%%%%%%%%%%%#(/*,,,*(#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#(/*////////////
.,***,,,,,,..............,,,**********,..,***//((##%%%%%%%%%%%%%%%%%%%%%%%##((##%%%%%%%%%%%%%%%%%%%%%%%%%##(((((((((###%%%%%#/**///////////
.*****,,,,,,,,,,,,,,,,,,,*************,..,*******/(#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%##///*//////((#%%%%%#(**///////////
.****************/******/***////*****,.,*///////**/#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#(////////////(#%%%%%#/**//////////
.***********************/////*******,..,*//////////(#%%%%%%%%%%%%%%%%%%%%##########%%%%%%%%%%%%%%%%%%%%#(///////////*/(#%%%%%#(***/////////
.************************///********,..,*//////////#%%%%%%%%%%%%%%%%%%#(//*****///(((##%%%%%%%%%%%%%%%%#(///////////**/##%%%%##/***////////
.***********************************,.,,***///////(#%%%%%%%%%%%%%%%%#(/*,,,*//((((////(#%%%%%%%%%%%%%%%#((////////////(#%%%%%%#(*********//
,***********,,,*,,*,,**************,,,*//******//(#%%%%%%%%%%%%%%%%%#(*,,*/(((#####(((((#%%%%%%%%%%%%%%%##///////////(#%%%%%%%%#(***///////
,*************,,**,,,************,,,,,/(##((((####%%%%%%%%%%%%%%%%%%%(/**/(((#((((#((//(#%%%%%%%%%%%%%%%%%#(((((((((##%%%%%%%%%%#/**///////
,******************************,,,,,,,*(#%#%%%%%%%%%%%%%%%%%%%%%%%%%%#(**/((#(#(((#((//(#%%%%%%%%%%%%%%%%%%%%%%%#%#%%%%%%%%%%%%%#(**///////
,*************,**************,****,,,,,/(#%%%%%%%%%%%%%%%%%%%%%%%%%%%%#(/*/((((#((((///(#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%(/*///////
,*************************************,*/#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%##(////////////(#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#/**/////*
,******////****///////////////////////***/#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%####(((((((###%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#(********
.,*,****///////////////////////////////***/#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#(/*******
.,,,,*****//////////////////////////*******(#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%##(*******
.,,,,,,***********/////////////////********/(#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%(*******
`);