chore: format curriculum (#37912)

This commit is contained in:
Oliver Eyton-Williams
2020-12-15 16:52:02 +01:00
committed by GitHub
parent 45fe3d8814
commit 22fbc62f88
52 changed files with 9021 additions and 0 deletions

View File

@ -8,6 +8,8 @@
"tools/challenge-md-parser/mdx",
"tools/scripts/seed",
"tools/scripts/build",
"tools/scripts/formatter",
"tools/scripts/formatter/fcc-md-to-gfm",
"tools/challenge-helper-scripts"
],
"version": "independent"

View File

@ -0,0 +1,35 @@
# How the formatter works
## Validation
There are two scripts that validate challenges: validate-text and
validate-hints. validate-text ensures that any code inside <code> tags can
be converted to backticks. For example <code>var x = 'y';</code> is fine, but
<code><span>not okay</span><code> is not, since `<span>not okay</span>` becomes
<code>&lt;span&gt;not okay&lt;/span&gt;<code> which is not the intention.
In contrast <code>&lt;span&gt;span we want&lt;/span&gt;<code> is perfectly fine.
The script will parse this and replace it with `<span>span we want</span>`.
Some challenges will break, so it's worth checking how they're rendered after
transform-challenges has done its work.
validate-hints operates differently. It checks to see if there is any markdown
syntax inside the test text and flags up challenges that *may* not be being
rendered correctly. Unfortunately there are a lot of false positives, a human
needs to see if the challenge author intended for the text to be parsed as
markdown or not. Also bare email addresses name@address.com should be
highlighted by this tool and will need wrapping with backticks.
## How to use
mmv is great, so I recommend installing that first. Then:
cd curriculum/challenges/chinese/12-certificates
mmv ";*.md" "#1#2.markdown"
cd ../../../../tools/scripts/formatter/fcc-md-to-gfm/
node formatCurriculum.js
cd ../md-to-mdx
node md-to-mdx # this might have warnings and errors. Errors need fixing, but warnings just need checking
cd ../../../../curriculum/challenges/chinese/
mmv -md ";*.mdx" "#1#2.md"

View File

@ -0,0 +1,130 @@
---
title: Amicable pairs
id: 5949b579404977fbaefcd737
challengeType: 5
forumTopicId: 302225
---
## Description
<section id='description'>
Two integers $N$ and $M$ are said to be <a href='https://en.wikipedia.org/wiki/Amicable numbers' title='wp: Amicable numbers' target='_blank'>amicable pairs</a> if $N \neq M$ and the sum of the <a href="https://rosettacode.org/wiki/Proper divisors" title="Proper divisors" target="_blank">proper divisors</a> of $N$ ($\mathrm{sum}(\mathrm{propDivs}(N))$) $= M$ as well as $\mathrm{sum}(\mathrm{propDivs}(M)) = N$.
<strong>Example:</strong>
<strong>1184</strong> and <strong>1210</strong> are an amicable pair, with proper divisors:
<ul>
<li>1, 2, 4, 8, 16, 32, 37, 74, 148, 296, 592 and</li>
<li>1, 2, 5, 10, 11, 22, 55, 110, 121, 242, 605 respectively.</li>
</ul>
</section>
## Instructions
<section id='instructions'>
Calculate and show here the Amicable pairs below 20,000 (there are eight).
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: <code>amicablePairsUpTo</code> should be a function.
testString: assert(typeof amicablePairsUpTo === 'function');
- text: <code>amicablePairsUpTo(300)</code> should return <code>[[220,284]]</code>.
testString: assert.deepEqual(amicablePairsUpTo(300), answer300);
- text: <code>amicablePairsUpTo(3000)</code> should return <code>[[220,284],[1184,1210],[2620,2924]]</code>.
testString: assert.deepEqual(amicablePairsUpTo(3000), answer3000);
- text: <code>amicablePairsUpTo(20000)</code> should return <code>[[220,284],[1184,1210],[2620,2924],[5020,5564],[6232,6368],[10744,10856],[12285,14595],[17296,18416]]</code>.
testString: assert.deepEqual(amicablePairsUpTo(20000), answer20000);
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='js-seed'>
```js
function amicablePairsUpTo(maxNum) {
// Good luck!
return true;
}
```
</div>
### After Test
<div id='js-teardown'>
```js
const answer300 = [[220, 284]];
const answer3000 = [
[220, 284],
[1184, 1210],
[2620, 2924]
];
const answer20000 = [
[220, 284],
[1184, 1210],
[2620, 2924],
[5020, 5564],
[6232, 6368],
[10744, 10856],
[12285, 14595],
[17296, 18416]
];
```
</div>
</section>
## Solution
<section id='solution'>
```js
// amicablePairsUpTo :: Int -> [(Int, Int)]
function amicablePairsUpTo(maxNum) {
return range(1, maxNum)
.map(x => properDivisors(x)
.reduce((a, b) => a + b, 0))
.reduce((a, m, i, lst) => {
const n = i + 1;
return (m > n) && lst[m - 1] === n ?
a.concat([
[n, m]
]) : a;
}, []);
}
// properDivisors :: Int -> [Int]
function properDivisors(n) {
if (n < 2) return [];
const rRoot = Math.sqrt(n);
const intRoot = Math.floor(rRoot);
const blnPerfectSquare = rRoot === intRoot;
const lows = range(1, intRoot)
.filter(x => (n % x) === 0);
return lows.concat(lows.slice(1)
.map(x => n / x)
.reverse()
.slice(blnPerfectSquare | 0));
}
// Int -> Int -> Maybe Int -> [Int]
function range(m, n, step) {
const d = (step || 1) * (n >= m ? 1 : -1);
return Array.from({
length: Math.floor((n - m) / d) + 1
}, (_, i) => m + (i * d));
}
```
</section>

View File

@ -0,0 +1,141 @@
---
title: Amicable pairs
id: 5949b579404977fbaefcd737
challengeType: 5
forumTopicId: 302225
---
## Description
<section id='description'>
Two integers $N$ and $M$ are said to be [amicable pairs](<https://en.wikipedia.org/wiki/Amicable numbers> "wp: Amicable numbers") if $N \\neq M$ and the sum of the [proper divisors](<https://rosettacode.org/wiki/Proper divisors> "Proper divisors") of $N$ ($\\mathrm{sum}(\\mathrm{propDivs}(N))$) $= M$ as well as $\\mathrm{sum}(\\mathrm{propDivs}(M)) = N$.
**Example:**
**1184** and **1210** are an amicable pair, with proper divisors:
<ul>
<li>1, 2, 4, 8, 16, 32, 37, 74, 148, 296, 592 and</li>
<li>1, 2, 5, 10, 11, 22, 55, 110, 121, 242, 605 respectively.</li>
</ul>
</section>
## Instructions
<section id='instructions'>
Calculate and show here the Amicable pairs below 20,000 (there are eight).
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: <code>amicablePairsUpTo</code> should be a function.
testString: assert(typeof amicablePairsUpTo === 'function');
- text: <code>amicablePairsUpTo(300)</code> should return <code>[[220,284]]</code>.
testString: assert.deepEqual(amicablePairsUpTo(300), answer300);
- text: <code>amicablePairsUpTo(3000)</code> should return <code>[[220,284],[1184,1210],[2620,2924]]</code>.
testString: assert.deepEqual(amicablePairsUpTo(3000), answer3000);
- text: <code>amicablePairsUpTo(20000)</code> should return <code>[[220,284],[1184,1210],[2620,2924],[5020,5564],[6232,6368],[10744,10856],[12285,14595],[17296,18416]]</code>.
testString: assert.deepEqual(amicablePairsUpTo(20000), answer20000);
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='js-seed'>
```js
function amicablePairsUpTo(maxNum) {
// Good luck!
return true;
}
```
</div>
### After Test
<div id='js-teardown'>
```js
const answer300 = [[220, 284]];
const answer3000 = [
[220, 284],
[1184, 1210],
[2620, 2924]
];
const answer20000 = [
[220, 284],
[1184, 1210],
[2620, 2924],
[5020, 5564],
[6232, 6368],
[10744, 10856],
[12285, 14595],
[17296, 18416]
];
```
</div>
</section>
## Solution
<section id='solution'>
```js
// amicablePairsUpTo :: Int -> [(Int, Int)]
function amicablePairsUpTo(maxNum) {
return range(1, maxNum)
.map(x => properDivisors(x)
.reduce((a, b) => a + b, 0))
.reduce((a, m, i, lst) => {
const n = i + 1;
return (m > n) && lst[m - 1] === n ?
a.concat([
[n, m]
]) : a;
}, []);
}
// properDivisors :: Int -> [Int]
function properDivisors(n) {
if (n < 2) return [];
const rRoot = Math.sqrt(n);
const intRoot = Math.floor(rRoot);
const blnPerfectSquare = rRoot === intRoot;
const lows = range(1, intRoot)
.filter(x => (n % x) === 0);
return lows.concat(lows.slice(1)
.map(x => n / x)
.reverse()
.slice(blnPerfectSquare | 0));
}
// Int -> Int -> Maybe Int -> [Int]
function range(m, n, step) {
const d = (step || 1) * (n >= m ? 1 : -1);
return Array.from({
length: Math.floor((n - m) / d) + 1
}, (_, i) => m + (i * d));
}
```
</section>

View File

@ -0,0 +1,103 @@
---
title: 9 billion names of God the integer
id: 5949b579404977fbaefcd736
challengeType: 5
forumTopicId: 302219
---
## Description
<section id='description'>
This task is a variation of the <a href='https://en.wikipedia.org/wiki/The Nine Billion Names of God#Plot_summary' title='wp: The Nine Billion Names of God#Plot_summary' target='_blank'>short story by Arthur C. Clarke</a>.
(Solvers should be aware of the consequences of completing this task.)
In detail, to specify what is meant by a "name":
<ul>
<li>The integer 1 has 1 name "1".</li>
<li>The integer 2 has 2 names "1+1" and "2".</li>
<li>The integer 3 has 3 names "1+1+1", "2+1", and "3".</li>
<li>The integer 4 has 5 names "1+1+1+1", "2+1+1", "2+2", "3+1", "4".</li>
<li>The integer 5 has 7 names "1+1+1+1+1", "2+1+1+1", "2+2+1", "3+1+1", "3+2", "4+1", "5".</li>
</ul>
This can be visualized in the following form:
<pre>
1
1 1
1 1 1
1 2 1 1
1 2 2 1 1
1 3 3 2 1 1
</pre>
Where row $n$ corresponds to integer $n$, and each column $C$ in row $m$ from left to right corresponds to the number of names beginning with $C$.
Optionally note that the sum of the $n$-th row $P(n)$ is the integer partition function.
</section>
## Instructions
<section id='instructions'>
Implement a function that returns the sum of the $n$-th row.
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: <code>numberOfNames</code> is a function.
testString: assert(typeof numberOfNames === 'function');
- text: <code>numberOfNames(5)</code> should equal 7.
testString: assert.equal(numberOfNames(5), 7);
- text: <code>numberOfNames(12)</code> should equal 77.
testString: assert.equal(numberOfNames(12), 77);
- text: <code>numberOfNames(18)</code> should equal 385.
testString: assert.equal(numberOfNames(18), 385);
- text: <code>numberOfNames(23)</code> should equal 1255.
testString: assert.equal(numberOfNames(23), 1255);
- text: <code>numberOfNames(42)</code> should equal 53174.
testString: assert.equal(numberOfNames(42), 53174);
- text: <code>numberOfNames(123)</code> should equal 2552338241.
testString: assert.equal(numberOfNames(123), 2552338241);
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='js-seed'>
```js
function numberOfNames(num) {
// Good luck!
return true;
}
```
</div>
</section>
## Solution
<section id='solution'>
```js
function numberOfNames(num) {
const cache = [
[1]
];
for (let l = cache.length; l < num + 1; l++) {
let Aa;
let Mi;
const r = [0];
for (let x = 1; x < l + 1; x++) {
r.push(r[r.length - 1] + (Aa = cache[l - x < 0 ? cache.length - (l - x) : l - x])[(Mi = Math.min(x, l - x)) < 0 ? Aa.length - Mi : Mi]);
}
cache.push(r);
}
return cache[num][cache[num].length - 1];
}
```
</section>

View File

@ -0,0 +1,115 @@
---
title: 9 billion names of God the integer
id: 5949b579404977fbaefcd736
challengeType: 5
forumTopicId: 302219
---
## Description
<section id='description'>
This task is a variation of the [short story by Arthur C. Clarke](<https://en.wikipedia.org/wiki/The Nine Billion Names of God#Plot_summary> "wp: The Nine Billion Names of God#Plot_summary").
(Solvers should be aware of the consequences of completing this task.)
In detail, to specify what is meant by a "name":
<ul>
<li>The integer 1 has 1 name "1".</li>
<li>The integer 2 has 2 names "1+1" and "2".</li>
<li>The integer 3 has 3 names "1+1+1", "2+1", and "3".</li>
<li>The integer 4 has 5 names "1+1+1+1", "2+1+1", "2+2", "3+1", "4".</li>
<li>The integer 5 has 7 names "1+1+1+1+1", "2+1+1+1", "2+2+1", "3+1+1", "3+2", "4+1", "5".</li>
</ul>
This can be visualized in the following form:
<pre> 1
1 1
1 1 1
1 2 1 1
1 2 2 1 1
1 3 3 2 1 1
</pre>
Where row $n$ corresponds to integer $n$, and each column $C$ in row $m$ from left to right corresponds to the number of names beginning with $C$.
Optionally note that the sum of the $n$-th row $P(n)$ is the integer partition function.
</section>
## Instructions
<section id='instructions'>
Implement a function that returns the sum of the $n$-th row.
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: <code>numberOfNames</code> is a function.
testString: assert(typeof numberOfNames === 'function');
- text: <code>numberOfNames(5)</code> should equal 7.
testString: assert.equal(numberOfNames(5), 7);
- text: <code>numberOfNames(12)</code> should equal 77.
testString: assert.equal(numberOfNames(12), 77);
- text: <code>numberOfNames(18)</code> should equal 385.
testString: assert.equal(numberOfNames(18), 385);
- text: <code>numberOfNames(23)</code> should equal 1255.
testString: assert.equal(numberOfNames(23), 1255);
- text: <code>numberOfNames(42)</code> should equal 53174.
testString: assert.equal(numberOfNames(42), 53174);
- text: <code>numberOfNames(123)</code> should equal 2552338241.
testString: assert.equal(numberOfNames(123), 2552338241);
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='js-seed'>
```js
function numberOfNames(num) {
// Good luck!
return true;
}
```
</div>
</section>
## Solution
<section id='solution'>
```js
function numberOfNames(num) {
const cache = [
[1]
];
for (let l = cache.length; l < num + 1; l++) {
let Aa;
let Mi;
const r = [0];
for (let x = 1; x < l + 1; x++) {
r.push(r[r.length - 1] + (Aa = cache[l - x < 0 ? cache.length - (l - x) : l - x])[(Mi = Math.min(x, l - x)) < 0 ? Aa.length - Mi : Mi]);
}
cache.push(r);
}
return cache[num][cache[num].length - 1];
}
```
</section>

View File

@ -0,0 +1,179 @@
---
id: bad87fee1348cd8acef08812
title: Create a Block Element Bootstrap Button
challengeType: 0
forumTopicId: 16810
---
## Description
<section id='description'>
Normally, your <code>button</code> elements with the <code>btn</code> and <code>btn-default</code> classes are only as wide as the text that they contain. For example:
<code>&lt;button class="btn btn-default"&gt;Submit&lt;/button&gt;</code>
This button would only be as wide as the word "Submit".
<button class='btn btn-default'>Submit</button>
By making them block elements with the additional class of <code>btn-block</code>, your button will stretch to fill your page's entire horizontal space and any elements following it will flow onto a "new line" below the block.
<code>&lt;button class="btn btn-default btn-block"&gt;Submit&lt;/button&gt;</code>
This button would take up 100% of the available width.
<button class='btn btn-default btn-block'>Submit</button>
Note that these buttons still need the <code>btn</code> class.
Add Bootstrap's <code>btn-block</code> class to your Bootstrap button.
</section>
## Instructions
<section id='instructions'>
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: Your button should still have the <code>btn</code> and <code>btn-default</code> classes.
testString: assert($("button").hasClass("btn") && $("button").hasClass("btn-default"));
- text: Your button should have the class <code>btn-block</code>.
testString: assert($("button").hasClass("btn-block"));
- text: All of your <code>button</code> elements should have closing tags.
testString: assert(code.match(/<\/button>/g) && code.match(/<button/g) && code.match(/<\/button>/g).length === code.match(/<button/g).length);
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='html-seed'>
```html
<link href="https://fonts.googleapis.com/css?family=Lobster" rel="stylesheet" type="text/css">
<style>
.red-text {
color: red;
}
h2 {
font-family: Lobster, Monospace;
}
p {
font-size: 16px;
font-family: Monospace;
}
.thick-green-border {
border-color: green;
border-width: 10px;
border-style: solid;
border-radius: 50%;
}
.smaller-image {
width: 100px;
}
</style>
<div class="container-fluid">
<h2 class="red-text text-center">CatPhotoApp</h2>
<p>Click here for <a href="#">cat photos</a>.</p>
<a href="#"><img class="smaller-image thick-green-border" src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back."></a>
<img src="https://bit.ly/fcc-running-cats" class="img-responsive" alt="Three kittens running towards the camera.">
<button class="btn btn-default">Like</button>
<p>Things cats love:</p>
<ul>
<li>cat nip</li>
<li>laser pointers</li>
<li>lasagna</li>
</ul>
<p>Top 3 things cats hate:</p>
<ol>
<li>flea treatment</li>
<li>thunder</li>
<li>other cats</li>
</ol>
<form action="/submit-cat-photo">
<label><input type="radio" name="indoor-outdoor"> Indoor</label>
<label><input type="radio" name="indoor-outdoor"> Outdoor</label>
<label><input type="checkbox" name="personality"> Loving</label>
<label><input type="checkbox" name="personality"> Lazy</label>
<label><input type="checkbox" name="personality"> Crazy</label>
<input type="text" placeholder="cat photo URL" required>
<button type="submit">Submit</button>
</form>
</div>
```
</div>
</section>
## Solution
<section id='solution'>
```html
<link href="https://fonts.googleapis.com/css?family=Lobster" rel="stylesheet" type="text/css">
<style>
.red-text {
color: red;
}
h2 {
font-family: Lobster, Monospace;
}
p {
font-size: 16px;
font-family: Monospace;
}
.thick-green-border {
border-color: green;
border-width: 10px;
border-style: solid;
border-radius: 50%;
}
.smaller-image {
width: 100px;
}
</style>
<div class="container-fluid">
<h2 class="red-text text-center">CatPhotoApp</h2>
<p>Click here for <a href="#">cat photos</a>.</p>
<a href="#"><img class="smaller-image thick-green-border" src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back."></a>
<img src="https://bit.ly/fcc-running-cats" class="img-responsive" alt="Three kittens running towards the camera.">
<button class="btn btn-block btn-default">Like</button>
<p>Things cats love:</p>
<ul>
<li>cat nip</li>
<li>laser pointers</li>
<li>lasagna</li>
</ul>
<p>Top 3 things cats hate:</p>
<ol>
<li>flea treatment</li>
<li>thunder</li>
<li>other cats</li>
</ol>
<form action="/submit-cat-photo">
<label><input type="radio" name="indoor-outdoor"> Indoor</label>
<label><input type="radio" name="indoor-outdoor"> Outdoor</label>
<label><input type="checkbox" name="personality"> Loving</label>
<label><input type="checkbox" name="personality"> Lazy</label>
<label><input type="checkbox" name="personality"> Crazy</label>
<input type="text" placeholder="cat photo URL" required>
<button type="submit">Submit</button>
</form>
</div>
```
</section>

View File

@ -0,0 +1,193 @@
---
id: bad87fee1348cd8acef08812
title: Create a Block Element Bootstrap Button
challengeType: 0
forumTopicId: 16810
---
## Description
<section id='description'>
Normally, your `button` elements with the `btn` and `btn-default` classes are only as wide as the text that they contain. For example:
`<button class="btn btn-default">Submit</button>`
This button would only be as wide as the word "Submit".
<button class='btn btn-default'>Submit</button>
By making them block elements with the additional class of `btn-block`, your button will stretch to fill your page's entire horizontal space and any elements following it will flow onto a "new line" below the block.
`<button class="btn btn-default btn-block">Submit</button>`
This button would take up 100% of the available width.
<button class='btn btn-default btn-block'>Submit</button>
Note that these buttons still need the `btn` class.
Add Bootstrap's `btn-block` class to your Bootstrap button.
</section>
## Instructions
<section id='instructions'>
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: Your button should still have the <code>btn</code> and <code>btn-default</code> classes.
testString: assert($("button").hasClass("btn") && $("button").hasClass("btn-default"));
- text: Your button should have the class <code>btn-block</code>.
testString: assert($("button").hasClass("btn-block"));
- text: All of your <code>button</code> elements should have closing tags.
testString: assert(code.match(/<\/button>/g) && code.match(/<button/g) && code.match(/<\/button>/g).length === code.match(/<button/g).length);
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='html-seed'>
```html
<link href="https://fonts.googleapis.com/css?family=Lobster" rel="stylesheet" type="text/css">
<style>
.red-text {
color: red;
}
h2 {
font-family: Lobster, Monospace;
}
p {
font-size: 16px;
font-family: Monospace;
}
.thick-green-border {
border-color: green;
border-width: 10px;
border-style: solid;
border-radius: 50%;
}
.smaller-image {
width: 100px;
}
</style>
<div class="container-fluid">
<h2 class="red-text text-center">CatPhotoApp</h2>
<p>Click here for <a href="#">cat photos</a>.</p>
<a href="#"><img class="smaller-image thick-green-border" src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back."></a>
<img src="https://bit.ly/fcc-running-cats" class="img-responsive" alt="Three kittens running towards the camera.">
<button class="btn btn-default">Like</button>
<p>Things cats love:</p>
<ul>
<li>cat nip</li>
<li>laser pointers</li>
<li>lasagna</li>
</ul>
<p>Top 3 things cats hate:</p>
<ol>
<li>flea treatment</li>
<li>thunder</li>
<li>other cats</li>
</ol>
<form action="/submit-cat-photo">
<label><input type="radio" name="indoor-outdoor"> Indoor</label>
<label><input type="radio" name="indoor-outdoor"> Outdoor</label>
<label><input type="checkbox" name="personality"> Loving</label>
<label><input type="checkbox" name="personality"> Lazy</label>
<label><input type="checkbox" name="personality"> Crazy</label>
<input type="text" placeholder="cat photo URL" required>
<button type="submit">Submit</button>
</form>
</div>
```
</div>
</section>
## Solution
<section id='solution'>
```html
<link href="https://fonts.googleapis.com/css?family=Lobster" rel="stylesheet" type="text/css">
<style>
.red-text {
color: red;
}
h2 {
font-family: Lobster, Monospace;
}
p {
font-size: 16px;
font-family: Monospace;
}
.thick-green-border {
border-color: green;
border-width: 10px;
border-style: solid;
border-radius: 50%;
}
.smaller-image {
width: 100px;
}
</style>
<div class="container-fluid">
<h2 class="red-text text-center">CatPhotoApp</h2>
<p>Click here for <a href="#">cat photos</a>.</p>
<a href="#"><img class="smaller-image thick-green-border" src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back."></a>
<img src="https://bit.ly/fcc-running-cats" class="img-responsive" alt="Three kittens running towards the camera.">
<button class="btn btn-block btn-default">Like</button>
<p>Things cats love:</p>
<ul>
<li>cat nip</li>
<li>laser pointers</li>
<li>lasagna</li>
</ul>
<p>Top 3 things cats hate:</p>
<ol>
<li>flea treatment</li>
<li>thunder</li>
<li>other cats</li>
</ol>
<form action="/submit-cat-photo">
<label><input type="radio" name="indoor-outdoor"> Indoor</label>
<label><input type="radio" name="indoor-outdoor"> Outdoor</label>
<label><input type="checkbox" name="personality"> Loving</label>
<label><input type="checkbox" name="personality"> Lazy</label>
<label><input type="checkbox" name="personality"> Crazy</label>
<input type="text" placeholder="cat photo URL" required>
<button type="submit">Submit</button>
</form>
</div>
```
</section>

View File

@ -0,0 +1,146 @@
---
id: bad87fee1348bd9aedf08805
title: Use CSS Selectors to Style Elements
challengeType: 0
videoUrl: 'https://scrimba.com/c/cJKMBT2'
forumTopicId: 18349
---
## Description
<section id='description'>
With CSS, there are hundreds of CSS properties that you can use to change the way an element looks on your page.
When you entered <code>&#60;h2 style="color: red;"&#62;CatPhotoApp&#60;/h2&#62;</code>, you were styling that individual <code>h2</code> element with inline CSS, which stands for Cascading Style Sheets.
That's one way to specify the style of an element, but there's a better way to apply CSS.
At the top of your code, create a <code>style</code> block like this:
```html
<style>
</style>
```
Inside that style block, you can create a <dfn>CSS selector</dfn> for all <code>h2</code> elements. For example, if you wanted all <code>h2</code> elements to be red, you would add a style rule that looks like this:
```html
<style>
h2 {
color: red;
}
</style>
```
Note that it's important to have both opening and closing curly braces (<code>{</code> and <code>}</code>) around each element's style rule(s). You also need to make sure that your element's style definition is between the opening and closing style tags. Finally, be sure to add a semicolon to the end of each of your element's style rules.
</section>
## Instructions
<section id='instructions'>
Delete your <code>h2</code> element's style attribute, and instead create a CSS <code>style</code> block. Add the necessary CSS to turn all <code>h2</code> elements blue.
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: The style attribute should be removed from your <code>h2</code> element.
testString: assert(!$("h2").attr("style"));
- text: You should create a <code>style</code> element.
testString: assert($("style") && $("style").length >= 1);
- text: Your <code>h2</code> element should be blue.
testString: assert($("h2").css("color") === "rgb(0, 0, 255)");
- text: Your stylesheet <code>h2</code> declaration should be valid with a semicolon and closing brace.
testString: assert(code.match(/h2\s*\{\s*color\s*:.*;\s*\}/g));
- text: All your <code>style</code> elements should be valid and have closing tags.
testString: assert(code.match(/<\/style>/g) && code.match(/<\/style>/g).length === (code.match(/<style((\s)*((type|media|scoped|title|disabled)="[^"]*")?(\s)*)*>/g) || []).length);
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='html-seed'>
```html
<h2 style="color: red;">CatPhotoApp</h2>
<main>
<p>Click here to view more <a href="#">cat photos</a>.</p>
<a href="#"><img src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back."></a>
<div>
<p>Things cats love:</p>
<ul>
<li>cat nip</li>
<li>laser pointers</li>
<li>lasagna</li>
</ul>
<p>Top 3 things cats hate:</p>
<ol>
<li>flea treatment</li>
<li>thunder</li>
<li>other cats</li>
</ol>
</div>
<form action="/submit-cat-photo">
<label><input type="radio" name="indoor-outdoor" checked> Indoor</label>
<label><input type="radio" name="indoor-outdoor"> Outdoor</label><br>
<label><input type="checkbox" name="personality" checked> Loving</label>
<label><input type="checkbox" name="personality"> Lazy</label>
<label><input type="checkbox" name="personality"> Energetic</label><br>
<input type="text" placeholder="cat photo URL" required>
<button type="submit">Submit</button>
</form>
</main>
```
</div>
</section>
## Solution
<section id='solution'>
```html
<style>
h2 {
color: blue;
}
</style>
<h2>CatPhotoApp</h2>
<main>
<p>Click here to view more <a href="#">cat photos</a>.</p>
<a href="#"><img src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back."></a>
<div>
<p>Things cats love:</p>
<ul>
<li>cat nip</li>
<li>laser pointers</li>
<li>lasagna</li>
</ul>
<p>Top 3 things cats hate:</p>
<ol>
<li>flea treatment</li>
<li>thunder</li>
<li>other cats</li>
</ol>
</div>
<form action="/submit-cat-photo">
<label><input type="radio" name="indoor-outdoor" checked> Indoor</label>
<label><input type="radio" name="indoor-outdoor"> Outdoor</label><br>
<label><input type="checkbox" name="personality" checked> Loving</label>
<label><input type="checkbox" name="personality"> Lazy</label>
<label><input type="checkbox" name="personality"> Energetic</label><br>
<input type="text" placeholder="cat photo URL" required>
<button type="submit">Submit</button>
</form>
</main>
```
</section>

View File

@ -0,0 +1,156 @@
---
id: bad87fee1348bd9aedf08805
title: Use CSS Selectors to Style Elements
challengeType: 0
videoUrl: 'https://scrimba.com/c/cJKMBT2'
forumTopicId: 18349
---
## Description
<section id='description'>
With CSS, there are hundreds of CSS properties that you can use to change the way an element looks on your page.
When you entered `<h2 style="color: red;">CatPhotoApp</h2>`, you were styling that individual `h2` element with inline CSS, which stands for Cascading Style Sheets.
That's one way to specify the style of an element, but there's a better way to apply CSS.
At the top of your code, create a `style` block like this:
```html
<style>
</style>
```
Inside that style block, you can create a <dfn>CSS selector</dfn> for all `h2` elements. For example, if you wanted all `h2` elements to be red, you would add a style rule that looks like this:
```html
<style>
h2 {
color: red;
}
</style>
```
Note that it's important to have both opening and closing curly braces (`{` and `}`) around each element's style rule(s). You also need to make sure that your element's style definition is between the opening and closing style tags. Finally, be sure to add a semicolon to the end of each of your element's style rules.
</section>
## Instructions
<section id='instructions'>
Delete your `h2` element's style attribute, and instead create a CSS `style` block. Add the necessary CSS to turn all `h2` elements blue.
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: The style attribute should be removed from your <code>h2</code> element.
testString: assert(!$("h2").attr("style"));
- text: You should create a <code>style</code> element.
testString: assert($("style") && $("style").length >= 1);
- text: Your <code>h2</code> element should be blue.
testString: assert($("h2").css("color") === "rgb(0, 0, 255)");
- text: Your stylesheet <code>h2</code> declaration should be valid with a semicolon and closing brace.
testString: assert(code.match(/h2\s*\{\s*color\s*:.*;\s*\}/g));
- text: All your <code>style</code> elements should be valid and have closing tags.
testString: assert(code.match(/<\/style>/g) && code.match(/<\/style>/g).length === (code.match(/<style((\s)*((type|media|scoped|title|disabled)="[^"]*")?(\s)*)*>/g) || []).length);
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='html-seed'>
```html
<h2 style="color: red;">CatPhotoApp</h2>
<main>
<p>Click here to view more <a href="#">cat photos</a>.</p>
<a href="#"><img src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back."></a>
<div>
<p>Things cats love:</p>
<ul>
<li>cat nip</li>
<li>laser pointers</li>
<li>lasagna</li>
</ul>
<p>Top 3 things cats hate:</p>
<ol>
<li>flea treatment</li>
<li>thunder</li>
<li>other cats</li>
</ol>
</div>
<form action="/submit-cat-photo">
<label><input type="radio" name="indoor-outdoor" checked> Indoor</label>
<label><input type="radio" name="indoor-outdoor"> Outdoor</label><br>
<label><input type="checkbox" name="personality" checked> Loving</label>
<label><input type="checkbox" name="personality"> Lazy</label>
<label><input type="checkbox" name="personality"> Energetic</label><br>
<input type="text" placeholder="cat photo URL" required>
<button type="submit">Submit</button>
</form>
</main>
```
</div>
</section>
## Solution
<section id='solution'>
```html
<style>
h2 {
color: blue;
}
</style>
<h2>CatPhotoApp</h2>
<main>
<p>Click here to view more <a href="#">cat photos</a>.</p>
<a href="#"><img src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back."></a>
<div>
<p>Things cats love:</p>
<ul>
<li>cat nip</li>
<li>laser pointers</li>
<li>lasagna</li>
</ul>
<p>Top 3 things cats hate:</p>
<ol>
<li>flea treatment</li>
<li>thunder</li>
<li>other cats</li>
</ol>
</div>
<form action="/submit-cat-photo">
<label><input type="radio" name="indoor-outdoor" checked> Indoor</label>
<label><input type="radio" name="indoor-outdoor"> Outdoor</label><br>
<label><input type="checkbox" name="personality" checked> Loving</label>
<label><input type="checkbox" name="personality"> Lazy</label>
<label><input type="checkbox" name="personality"> Energetic</label><br>
<input type="text" placeholder="cat photo URL" required>
<button type="submit">Submit</button>
</form>
</main>
```
</section>

View File

@ -0,0 +1,71 @@
---
id: bad87fee1348bd9aedf08817
title: Make Dead Links Using the Hash Symbol
challengeType: 0
videoUrl: 'https://scrimba.com/p/pVMPUv/cMdkytL'
forumTopicId: 18230
---
## Description
<section id='description'>
Sometimes you want to add <code>a</code> elements to your website before you know where they will link.
This is also handy when you're changing the behavior of a link using <code>JavaScript</code>, which we'll learn about later.
</section>
## Instructions
<section id='instructions'>
The current value of the <code>href</code> attribute is a link that points to "http://freecatphotoapp.com". Replace the <code>href</code> attribute value with a <code>#</code>, also known as a hash symbol, to create a dead link.
For example: <code>href="#"</code>
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: Your <code>a</code> element should be a dead link with the value of the <code>href</code> attribute set to "#".
testString: assert($("a").attr("href") === "#");
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='html-seed'>
```html
<h2>CatPhotoApp</h2>
<main>
<p>Click here to view more <a href="http://freecatphotoapp.com" target="_blank">cat photos</a>.</p>
<img src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back.">
<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
<p>Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.</p>
</main>
```
</div>
</section>
## Solution
<section id='solution'>
```html
<h2>CatPhotoApp</h2>
<main>
<p>Click here to view more <a href="#" target="_blank">cat photos</a>.</p>
<img src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back.">
<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
<p>Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.</p>
</main>
```
</section>

View File

@ -0,0 +1,80 @@
---
id: bad87fee1348bd9aedf08817
title: Make Dead Links Using the Hash Symbol
challengeType: 0
videoUrl: 'https://scrimba.com/p/pVMPUv/cMdkytL'
forumTopicId: 18230
---
## Description
<section id='description'>
Sometimes you want to add `a` elements to your website before you know where they will link.
This is also handy when you're changing the behavior of a link using `JavaScript`, which we'll learn about later.
</section>
## Instructions
<section id='instructions'>
The current value of the `href` attribute is a link that points to "`http://freecatphotoapp.com`". Replace the `href` attribute value with a `#`, also known as a hash symbol, to create a dead link.
For example: `href="#"`
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: Your <code>a</code> element should be a dead link with the value of the <code>href</code> attribute set to "#".
testString: assert($("a").attr("href") === "#");
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='html-seed'>
```html
<h2>CatPhotoApp</h2>
<main>
<p>Click here to view more <a href="http://freecatphotoapp.com" target="_blank">cat photos</a>.</p>
<img src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back.">
<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
<p>Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.</p>
</main>
```
</div>
</section>
## Solution
<section id='solution'>
```html
<h2>CatPhotoApp</h2>
<main>
<p>Click here to view more <a href="#" target="_blank">cat photos</a>.</p>
<img src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back.">
<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
<p>Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.</p>
</main>
```
</section>

View File

@ -0,0 +1,83 @@
---
id: 5a23c84252665b21eecc8000
title: Sort disjoint sublist
challengeType: 5
forumTopicId: 302307
---
## Description
<section id='description'>
Given a list of values and a set of integer indices into that value list, the task is to sort the values at the given indices, but preserving the values at indices outside the set of those to be sorted.
Make your function work with the following list of values and set of indices:
<code> values: [7, <b>6</b>, 5, 4, 3, 2, <b>1</b>, <b>0</b>]</code>
<code> indices(0-based): {6, 1, 7}</code>
Where the correct result would be:
<code>[7, <b>0</b>, 5, 4, 3, 2, <b>1</b>, <b>6</b>]</code>.
</section>
## Instructions
<section id='instructions'>
</section>
## Tests
<section id='tests'>
``` yml
tests:
- text: <code>sortDisjoint</code> should be a function.
testString: assert(typeof sortDisjoint == 'function', '<code>sortDisjoint</code> should be a function.');
- text: <code>sortDisjoint([7, 6, 5, 4, 3, 2, 1, 0], [6, 1, 7])</code> should return an array.
testString: assert(Array.isArray(sortDisjoint([7, 6, 5, 4, 3, 2, 1, 0], [6, 1, 7])), '<code>sortDisjoint([7, 6, 5, 4, 3, 2, 1, 0], [6, 1, 7])</code> should return an array.');
- text: <code>sortDisjoint([7, 6, 5, 4, 3, 2, 1, 0], [6, 1, 7])</code> should return <code>[7, 0, 5, 4, 3, 2, 1, 6]</code>.
testString: assert.deepEqual(sortDisjoint([7, 6, 5, 4, 3, 2, 1, 0], [6, 1, 7]), [7, 0, 5, 4, 3, 2, 1, 6], '<code>sortDisjoint([7, 6, 5, 4, 3, 2, 1, 0], [6, 1, 7])</code> should return <code>[7, 0, 5, 4, 3, 2, 1, 6]</code>.');
- text: <code>sortDisjoint([7, 6, 5, 4, 3, 2, 1, 0], [1, 2, 5, 6])</code> should return <code>[7, 1, 2, 4, 3, 5, 6, 0]</code>.
testString: assert.deepEqual(sortDisjoint([7, 6, 5, 4, 3, 2, 1, 0], [1, 2, 5, 6]), [7, 1, 2, 4, 3, 5, 6, 0], '<code>sortDisjoint([7, 6, 5, 4, 3, 2, 1, 0], [1, 2, 5, 6])</code> should return <code>[7, 1, 2, 4, 3, 5, 6, 0]</code>.');
- text: <code>sortDisjoint([8, 7, 6, 5, 4, 3, 2, 1], [6, 1, 7])</code> should return <code>[8, 1, 6, 5, 4, 3, 2, 7]</code>.
testString: assert.deepEqual(sortDisjoint([8, 7, 6, 5, 4, 3, 2, 1], [6, 1, 7]), [8, 1, 6, 5, 4, 3, 2, 7], '<code>sortDisjoint([8, 7, 6, 5, 4, 3, 2, 1], [6, 1, 7])</code> should return <code>[8, 1, 6, 5, 4, 3, 2, 7]</code>.');
- text: <code>sortDisjoint([8, 7, 6, 5, 4, 3, 2, 1], [1, 3, 5, 6])</code> should return <code>[8, 2, 6, 3, 4, 5, 7, 1]</code>.
testString: assert.deepEqual(sortDisjoint([8, 7, 6, 5, 4, 3, 2, 1], [1, 3, 5, 6]), [8, 2, 6, 3, 4, 5, 7, 1], '<code>sortDisjoint([8, 7, 6, 5, 4, 3, 2, 1], [1, 3, 5, 6])</code> should return <code>[8, 2, 6, 3, 4, 5, 7, 1]</code>.');
- text: <code>sortDisjoint([6, 1, 7, 1, 3, 5, 6], [6, 1, 5, 4])</code> should return <code>[6, 1, 7, 1, 3, 5, 6]</code>.
testString: assert.deepEqual(sortDisjoint([6, 1, 7, 1, 3, 5, 6], [6, 1, 5, 4]), [6, 1, 7, 1, 3, 5, 6],'<code>sortDisjoint([6, 1, 7, 1, 3, 5, 6], [6, 1, 5, 4])</code> should return <code>[6, 1, 7, 1, 3, 5, 6]</code>.');
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='js-seed'>
```js
function sortDisjoint(values, indices) {
// Good luck!
}
```
</div>
</section>
## Solution
<section id='solution'>
```js
function sortDisjoint(values, indices) {
let sublist = [];
indices.sort(function (a, b) { return a - b; });
for (let i = 0; i < indices.length; i++) {
sublist.push(values[indices[i]]);
}
sublist.sort((a, b) => { return a - b; });
for (let i = 0; i < indices.length; i++) {
values[indices[i]] = sublist[i];
}
return values;
}
```
</section>

View File

@ -0,0 +1,95 @@
---
id: 5a23c84252665b21eecc8000
title: Sort disjoint sublist
challengeType: 5
forumTopicId: 302307
---
## Description
<section id='description'>
Given a list of values and a set of integer indices into that value list, the task is to sort the values at the given indices, but preserving the values at indices outside the set of those to be sorted.
Make your function work with the following list of values and set of indices:
<code>values: [7, <b>6</b>, 5, 4, 3, 2, <b>1</b>, <b>0</b>]</code>
`indices(0-based): {6, 1, 7}`
Where the correct result would be:
<code>[7, <b>0</b>, 5, 4, 3, 2, <b>1</b>, <b>6</b>]</code>.
</section>
## Instructions
<section id='instructions'>
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: <code>sortDisjoint</code> should be a function.
testString: assert(typeof sortDisjoint == 'function', '<code>sortDisjoint</code> should be a function.');
- text: <code>sortDisjoint([7, 6, 5, 4, 3, 2, 1, 0], [6, 1, 7])</code> should return an array.
testString: assert(Array.isArray(sortDisjoint([7, 6, 5, 4, 3, 2, 1, 0], [6, 1, 7])), '<code>sortDisjoint([7, 6, 5, 4, 3, 2, 1, 0], [6, 1, 7])</code> should return an array.');
- text: <code>sortDisjoint([7, 6, 5, 4, 3, 2, 1, 0], [6, 1, 7])</code> should return <code>[7, 0, 5, 4, 3, 2, 1, 6]</code>.
testString: assert.deepEqual(sortDisjoint([7, 6, 5, 4, 3, 2, 1, 0], [6, 1, 7]), [7, 0, 5, 4, 3, 2, 1, 6], '<code>sortDisjoint([7, 6, 5, 4, 3, 2, 1, 0], [6, 1, 7])</code> should return <code>[7, 0, 5, 4, 3, 2, 1, 6]</code>.');
- text: <code>sortDisjoint([7, 6, 5, 4, 3, 2, 1, 0], [1, 2, 5, 6])</code> should return <code>[7, 1, 2, 4, 3, 5, 6, 0]</code>.
testString: assert.deepEqual(sortDisjoint([7, 6, 5, 4, 3, 2, 1, 0], [1, 2, 5, 6]), [7, 1, 2, 4, 3, 5, 6, 0], '<code>sortDisjoint([7, 6, 5, 4, 3, 2, 1, 0], [1, 2, 5, 6])</code> should return <code>[7, 1, 2, 4, 3, 5, 6, 0]</code>.');
- text: <code>sortDisjoint([8, 7, 6, 5, 4, 3, 2, 1], [6, 1, 7])</code> should return <code>[8, 1, 6, 5, 4, 3, 2, 7]</code>.
testString: assert.deepEqual(sortDisjoint([8, 7, 6, 5, 4, 3, 2, 1], [6, 1, 7]), [8, 1, 6, 5, 4, 3, 2, 7], '<code>sortDisjoint([8, 7, 6, 5, 4, 3, 2, 1], [6, 1, 7])</code> should return <code>[8, 1, 6, 5, 4, 3, 2, 7]</code>.');
- text: <code>sortDisjoint([8, 7, 6, 5, 4, 3, 2, 1], [1, 3, 5, 6])</code> should return <code>[8, 2, 6, 3, 4, 5, 7, 1]</code>.
testString: assert.deepEqual(sortDisjoint([8, 7, 6, 5, 4, 3, 2, 1], [1, 3, 5, 6]), [8, 2, 6, 3, 4, 5, 7, 1], '<code>sortDisjoint([8, 7, 6, 5, 4, 3, 2, 1], [1, 3, 5, 6])</code> should return <code>[8, 2, 6, 3, 4, 5, 7, 1]</code>.');
- text: <code>sortDisjoint([6, 1, 7, 1, 3, 5, 6], [6, 1, 5, 4])</code> should return <code>[6, 1, 7, 1, 3, 5, 6]</code>.
testString: assert.deepEqual(sortDisjoint([6, 1, 7, 1, 3, 5, 6], [6, 1, 5, 4]), [6, 1, 7, 1, 3, 5, 6],'<code>sortDisjoint([6, 1, 7, 1, 3, 5, 6], [6, 1, 5, 4])</code> should return <code>[6, 1, 7, 1, 3, 5, 6]</code>.');
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='js-seed'>
```js
function sortDisjoint(values, indices) {
// Good luck!
}
```
</div>
</section>
## Solution
<section id='solution'>
```js
function sortDisjoint(values, indices) {
let sublist = [];
indices.sort(function (a, b) { return a - b; });
for (let i = 0; i < indices.length; i++) {
sublist.push(values[indices[i]]);
}
sublist.sort((a, b) => { return a - b; });
for (let i = 0; i < indices.length; i++) {
values[indices[i]] = sublist[i];
}
return values;
}
```
</section>

View File

@ -0,0 +1,53 @@
---
id: 587d7dbc367417b2b2512bae
title: Build a Drum Machine
isRequired: true
challengeType: 3
forumTopicId: 301370
---
## Description
<section id='description'>
<strong>Objective:</strong> Build a <a href='https://codepen.io' target='_blank'>CodePen.io</a> app that is functionally similar to this: <a href='https://codepen.io/freeCodeCamp/full/MJyNMd' target='_blank'>https://codepen.io/freeCodeCamp/full/MJyNMd</a>.
Fulfill the below <a href='https://en.wikipedia.org/wiki/User_story' target='_blank'>user stories</a> and get all of the tests to pass. Give it your own personal style.
You can use any mix of HTML, JavaScript, CSS, Bootstrap, SASS, React, Redux, and jQuery to complete this project. You should use a frontend framework (like React for example) because this section is about learning frontend frameworks. Additional technologies not listed above are not recommended and using them is at your own risk. We are looking at supporting other frontend frameworks like Angular and Vue, but they are not currently supported. We will accept and try to fix all issue reports that use the suggested technology stack for this project. Happy coding!
<strong>User Story #1:</strong> I should be able to see an outer container with a corresponding <code>id="drum-machine"</code> that contains all other elements.
<strong>User Story #2:</strong> Within <code>#drum-machine</code> I can see an element with a corresponding <code>id="display"</code>.
<strong>User Story #3:</strong> Within <code>#drum-machine</code> I can see 9 clickable drum pad elements, each with a class name of <code>drum-pad</code>, a unique id that describes the audio clip the drum pad will be set up to trigger, and an inner text that corresponds to one of the following keys on the keyboard: Q, W, E, A, S, D, Z, X, C. The drum pads MUST be in this order.
<strong>User Story #4:</strong> Within each <code>.drum-pad</code>, there should be an HTML5 <code>audio</code> element which has a <code>src</code> attribute pointing to an audio clip, a class name of <code>clip</code>, and an id corresponding to the inner text of its parent <code>.drum-pad</code> (e.g. <code>id="Q"</code>, <code>id="W"</code>, <code>id="E"</code> etc.).
<strong>User Story #5:</strong> When I click on a <code>.drum-pad</code> element, the audio clip contained in its child <code>audio</code> element should be triggered.
<strong>User Story #6:</strong> When I press the trigger key associated with each <code>.drum-pad</code>, the audio clip contained in its child <code>audio</code> element should be triggered (e.g. pressing the Q key should trigger the drum pad which contains the string "Q", pressing the W key should trigger the drum pad which contains the string "W", etc.).
<strong>User Story #7:</strong> When a <code>.drum-pad</code> is triggered, a string describing the associated audio clip is displayed as the inner text of the <code>#display</code> element (each string must be unique).
You can build your project by forking <a href='http://codepen.io/freeCodeCamp/pen/MJjpwO' target='_blank'>this CodePen pen</a>. Or you can use this CDN link to run the tests in any environment you like: <code>https://cdn.freecodecamp.org/testable-projects-fcc/v1/bundle.js</code>
Once you're done, submit the URL to your working project with all its tests passing.
Remember to use the <a href='https://forum.freecodecamp.org/t/how-to-get-help-when-you-are-stuck/19514' target='_blank'>Read-Search-Ask</a> method if you get stuck.
</section>
## Instructions
<section id='instructions'>
</section>
## Tests
<section id='tests'>
```yml
tests: []
```
</section>
## Challenge Seed
<section id='challengeSeed'>
</section>
## Solution
<section id='solution'>
```js
// solution required
```
</section>

View File

@ -0,0 +1,72 @@
---
id: 587d7dbc367417b2b2512bae
title: Build a Drum Machine
isRequired: true
challengeType: 3
forumTopicId: 301370
---
## Description
<section id='description'>
**Objective:** Build a [CodePen.io](https://codepen.io) app that is functionally similar to this: <https://codepen.io/freeCodeCamp/full/MJyNMd>.
Fulfill the below [user stories](https://en.wikipedia.org/wiki/User_story) and get all of the tests to pass. Give it your own personal style.
You can use any mix of HTML, JavaScript, CSS, Bootstrap, SASS, React, Redux, and jQuery to complete this project. You should use a frontend framework (like React for example) because this section is about learning frontend frameworks. Additional technologies not listed above are not recommended and using them is at your own risk. We are looking at supporting other frontend frameworks like Angular and Vue, but they are not currently supported. We will accept and try to fix all issue reports that use the suggested technology stack for this project. Happy coding!
**User Story #1:** I should be able to see an outer container with a corresponding `id="drum-machine"` that contains all other elements.
**User Story #2:** Within `#drum-machine` I can see an element with a corresponding `id="display"`.
**User Story #3:** Within `#drum-machine` I can see 9 clickable drum pad elements, each with a class name of `drum-pad`, a unique id that describes the audio clip the drum pad will be set up to trigger, and an inner text that corresponds to one of the following keys on the keyboard: Q, W, E, A, S, D, Z, X, C. The drum pads MUST be in this order.
**User Story #4:** Within each `.drum-pad`, there should be an HTML5 `audio` element which has a `src` attribute pointing to an audio clip, a class name of `clip`, and an id corresponding to the inner text of its parent `.drum-pad` (e.g. `id="Q"`, `id="W"`, `id="E"` etc.).
**User Story #5:** When I click on a `.drum-pad` element, the audio clip contained in its child `audio` element should be triggered.
**User Story #6:** When I press the trigger key associated with each `.drum-pad`, the audio clip contained in its child `audio` element should be triggered (e.g. pressing the Q key should trigger the drum pad which contains the string "Q", pressing the W key should trigger the drum pad which contains the string "W", etc.).
**User Story #7:** When a `.drum-pad` is triggered, a string describing the associated audio clip is displayed as the inner text of the `#display` element (each string must be unique).
You can build your project by forking [this CodePen pen](http://codepen.io/freeCodeCamp/pen/MJjpwO). Or you can use this CDN link to run the tests in any environment you like: `https://cdn.freecodecamp.org/testable-projects-fcc/v1/bundle.js`
Once you're done, submit the URL to your working project with all its tests passing.
Remember to use the [Read-Search-Ask](https://forum.freecodecamp.org/t/how-to-get-help-when-you-are-stuck/19514) method if you get stuck.
</section>
## Instructions
<section id='instructions'>
</section>
## Tests
<section id='tests'>
```yml
tests: []
```
</section>
## Challenge Seed
<section id='challengeSeed'>
</section>
## Solution
<section id='solution'>
```js
// solution required
```
</section>

View File

@ -0,0 +1,91 @@
---
title: Entropy
id: 599d15309e88c813a40baf58
challengeType: 5
forumTopicId: 302254
---
## Description
<section id='description'>
Calculate the Shannon entropy H of a given input string.
Given the discreet random variable $X$ that is a string of $N$ "symbols" (total characters) consisting of $n$ different characters (n=2 for binary), the Shannon entropy of X in bits/symbol is:
$H_2(X) = -\sum_{i=1}^n \frac{count_i}{N} \log_2 \left(\frac{count_i}{N}\right)$
where $count_i$ is the count of character $n_i$.
A bit extra <> to test http://bare-url-handling.com!
</section>
## Instructions
<section id='instructions'>
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: <code>entropy</code> should be a function.
testString: assert(typeof entropy === 'function');
- text: <code>entropy("0")</code> should return <code>0</code>
testString: assert.equal(entropy('0'), 0);
- text: <code>entropy("01")</code> should return <code>1</code>
testString: assert.equal(entropy('01'), 1);
- text: <code>entropy("0123")</code> should return <code>2</code>
testString: assert.equal(entropy('0123'), 2);
- text: <code>entropy("01234567")</code> should return <code>3</code>
testString: assert.equal(entropy('01234567'), 3);
- text: <code>entropy("0123456789abcdef")</code> should return <code>4</code>
testString: assert.equal(entropy('0123456789abcdef'), 4);
- text: <code>entropy("1223334444")</code> should return <code>1.8464393446710154</code>
testString: assert.equal(entropy('1223334444'), 1.8464393446710154);
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='js-seed'>
```js
function entropy(s) {
// Good luck!
}
```
</div>
</section>
## Solution
<section id='solution'>
```js
function entropy(s) {
// Create a dictionary of character frequencies and iterate over it.
function process(s, evaluator) {
let h = Object.create(null),
k;
s.split('').forEach(c => {
h[c] && h[c]++ || (h[c] = 1); });
if (evaluator) for (k in h) evaluator(k, h[k]);
return h;
}
// Measure the entropy of a string in bits per symbol.
let sum = 0,
len = s.length;
process(s, (k, f) => {
const p = f / len;
sum -= p * Math.log(p) / Math.log(2);
});
return sum;
}
```
</section>

View File

@ -0,0 +1,99 @@
---
title: Entropy
id: 599d15309e88c813a40baf58
challengeType: 5
forumTopicId: 302254
---
## Description
<section id='description'>
Calculate the Shannon entropy H of a given input string.
Given the discreet random variable $X$ that is a string of $N$ "symbols" (total characters) consisting of $n$ different characters (n=2 for binary), the Shannon entropy of X in bits/symbol is:
$H_2(X) = -\\sum\_{i=1}^n \\frac{count_i}{N} \\log_2 \\left(\\frac{count_i}{N}\\right)$
where $count_i$ is the count of character $n_i$.
A bit extra &lt;> to test `http://bare-url-handling.com`!
</section>
## Instructions
<section id='instructions'>
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: <code>entropy</code> should be a function.
testString: assert(typeof entropy === 'function');
- text: <code>entropy("0")</code> should return <code>0</code>
testString: assert.equal(entropy('0'), 0);
- text: <code>entropy("01")</code> should return <code>1</code>
testString: assert.equal(entropy('01'), 1);
- text: <code>entropy("0123")</code> should return <code>2</code>
testString: assert.equal(entropy('0123'), 2);
- text: <code>entropy("01234567")</code> should return <code>3</code>
testString: assert.equal(entropy('01234567'), 3);
- text: <code>entropy("0123456789abcdef")</code> should return <code>4</code>
testString: assert.equal(entropy('0123456789abcdef'), 4);
- text: <code>entropy("1223334444")</code> should return <code>1.8464393446710154</code>
testString: assert.equal(entropy('1223334444'), 1.8464393446710154);
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='js-seed'>
```js
function entropy(s) {
// Good luck!
}
```
</div>
</section>
## Solution
<section id='solution'>
```js
function entropy(s) {
// Create a dictionary of character frequencies and iterate over it.
function process(s, evaluator) {
let h = Object.create(null),
k;
s.split('').forEach(c => {
h[c] && h[c]++ || (h[c] = 1); });
if (evaluator) for (k in h) evaluator(k, h[k]);
return h;
}
// Measure the entropy of a string in bits per symbol.
let sum = 0,
len = s.length;
process(s, (k, f) => {
const p = f / len;
sum -= p * Math.log(p) / Math.log(2);
});
return sum;
}
```
</section>

View File

@ -0,0 +1,63 @@
---
id: bd7123c8c441eddfaeb5bdef
title: Say Hello to HTML Elements
challengeType: 0
videoUrl: 'https://scrimba.com/p/pVMPUv/cE8Gpt2'
forumTopicId: 18276
---
## Description
<section id='description'>
Welcome to freeCodeCamp's HTML coding challenges. These will walk you through web development step-by-step.
First, you'll start by building a simple web page using HTML. You can edit code in your code editor, which is embedded into this web page.
Do you see the code in your code editor that says <code>&#60;h1&#62;Hello&#60;/h1&#62;</code>? That's an HTML element.
Most HTML elements have an opening tag and a closing tag.
Opening tags look like this:
<code>&#60;h1&#62;</code>
Closing tags look like this:
<code>&#60;/h1&#62;</code>
The only difference between opening and closing tags is the forward slash after the opening bracket of a closing tag.
Each challenge has tests you can run at any time by clicking the "Run tests" button. When you pass all tests, you'll be prompted to submit your solution and go to the next coding challenge.
<strong>Note:</strong>&nbsp;Testing non-breaking note stuff.
</section>
## Instructions
<section id='instructions'>
To pass the test on this challenge, change your <code>h1</code> element's text to say "Hello World".
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: Your <code>h1</code> element should have the text "Hello World".
testString: assert.isTrue((/hello(\s)+world/gi).test($('h1').text()));
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='html-seed'>
```html
<h1>Hello</h1>
```
</div>
</section>
## Solution
<section id='solution'>
```html
<h1>Hello World</h1>
```
</section>

View File

@ -0,0 +1,80 @@
---
id: bd7123c8c441eddfaeb5bdef
title: Say Hello to HTML Elements
challengeType: 0
videoUrl: 'https://scrimba.com/p/pVMPUv/cE8Gpt2'
forumTopicId: 18276
---
## Description
<section id='description'>
Welcome to freeCodeCamp's HTML coding challenges. These will walk you through web development step-by-step.
First, you'll start by building a simple web page using HTML. You can edit code in your code editor, which is embedded into this web page.
Do you see the code in your code editor that says `<h1>Hello</h1>`? That's an HTML element.
Most HTML elements have an opening tag and a closing tag.
Opening tags look like this:
`<h1>`
Closing tags look like this:
`</h1>`
The only difference between opening and closing tags is the forward slash after the opening bracket of a closing tag.
Each challenge has tests you can run at any time by clicking the "Run tests" button. When you pass all tests, you'll be prompted to submit your solution and go to the next coding challenge.
**Note:** Testing non-breaking note stuff.
</section>
## Instructions
<section id='instructions'>
To pass the test on this challenge, change your `h1` element's text to say "Hello World".
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: Your <code>h1</code> element should have the text "Hello World".
testString: assert.isTrue((/hello(\s)+world/gi).test($('h1').text()));
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='html-seed'>
```html
<h1>Hello</h1>
```
</div>
</section>
## Solution
<section id='solution'>
```html
<h1>Hello World</h1>
```
</section>

View File

@ -0,0 +1,136 @@
---
title: Hofstadter Figure-Figure sequences
id: 59622f89e4e137560018a40e
challengeType: 5
forumTopicId: 302286
---
## Description
<section id='description'>
These two sequences of positive integers are defined as:
<span style="margin-left: 2em;"><big>$R(1)=1\ ;\ S(1)=2 \\R(n)=R(n-1)+S(n-1), \quad n>1.$</big></span>
The sequence <big>$S(n)$</big> is further defined as the sequence of positive integers not present in <big>$R(n)$</big>.
Sequence <big>$R$</big> starts:
<pre>1, 3, 7, 12, 18, ...</pre>
Sequence <big>$S$</big> starts:
<pre>2, 4, 5, 6, 8, ...</pre>
</section>
## Instructions
<section id='instructions'>
Create two functions named <code>ffr</code> and <code>ffs</code> that when given <code>n</code> return <code>R(n)</code> or <code>S(n)</code> respectively. (Note that R(1) = 1 and S(1) = 2 to avoid off-by-one errors).
No maximum value for <code>n</code> should be assumed.
<strong>References</strong>
<ul>
<li>
Sloane's <a href='https://oeis.org/A005228' target='_blank'>A005228</a> and <a href='https://oeis.org/A030124' target='_blank'>A030124</a>.
</li>
<li>
Wikipedia: <a href='https://en.wikipedia.org/wiki/Hofstadter_sequence#Hofstadter_Figure-Figure_sequences' title='wp: Hofstadter_sequence#Hofstadter_Figure-Figure_sequences' target='_blank'>Hofstadter Figure-Figure sequences</a>.
</li>
</ul>
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: <code>ffr</code> should be a function.
testString: assert(typeof ffr === 'function');
- text: <code>ffs</code> should be a function.
testString: assert(typeof ffs === 'function');
- text: <code>ffr</code> should return integer.
testString: assert(Number.isInteger(ffr(1)));
- text: <code>ffs</code> should return integer.
testString: assert(Number.isInteger(ffs(1)));
- text: <code>ffr()</code> should return <code>69</code>
testString: assert.equal(ffr(ffrParamRes[0][0]), ffrParamRes[0][1]);
- text: <code>ffr()</code> should return <code>1509</code>
testString: assert.equal(ffr(ffrParamRes[1][0]), ffrParamRes[1][1]);
- text: <code>ffr()</code> should return <code>5764</code>
testString: assert.equal(ffr(ffrParamRes[2][0]), ffrParamRes[2][1]);
- text: <code>ffr()</code> should return <code>526334</code>
testString: assert.equal(ffr(ffrParamRes[3][0]), ffrParamRes[3][1]);
- text: <code>ffs()</code> should return <code>14</code>
testString: assert.equal(ffs(ffsParamRes[0][0]), ffsParamRes[0][1]);
- text: <code>ffs()</code> should return <code>59</code>
testString: assert.equal(ffs(ffsParamRes[1][0]), ffsParamRes[1][1]);
- text: <code>ffs()</code> should return <code>112</code>
testString: assert.equal(ffs(ffsParamRes[2][0]), ffsParamRes[2][1]);
- text: <code>ffs()</code> should return <code>1041</code>
testString: assert.equal(ffs(ffsParamRes[3][0]), ffsParamRes[3][1]);
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='js-seed'>
```js
// noprotect
function ffr(n) {
return n;
}
function ffs(n) {
return n;
}
```
</div>
### After Test
<div id='js-teardown'>
```js
const ffrParamRes = [[10, 69], [50, 1509], [100, 5764], [1000, 526334]];
const ffsParamRes = [[10, 14], [50, 59], [100, 112], [1000, 1041]];
```
</div>
</section>
## Solution
<section id='solution'>
```js
// noprotect
const R = [null, 1];
const S = [null, 2];
function extendSequences (n) {
let current = Math.max(R[R.length - 1], S[S.length - 1]);
let i;
while (R.length <= n || S.length <= n) {
i = Math.min(R.length, S.length) - 1;
current += 1;
if (current === R[i] + S[i]) {
R.push(current);
} else {
S.push(current);
}
}
}
function ffr (n) {
extendSequences(n);
return R[n];
}
function ffs (n) {
extendSequences(n);
return S[n];
}
```
</section>

View File

@ -0,0 +1,152 @@
---
title: Hofstadter Figure-Figure sequences
id: 59622f89e4e137560018a40e
challengeType: 5
forumTopicId: 302286
---
## Description
<section id='description'>
These two sequences of positive integers are defined as:
$R(1)=1\\ ;\\ S(1)=2 \\\\R(n)=R(n-1)+S(n-1), \\quad n>1.$
The sequence $S(n)$ is further defined as the sequence of positive integers not present in $R(n)$.
Sequence $R$ starts:
<pre>1, 3, 7, 12, 18, ...</pre>
Sequence $S$ starts:
<pre>2, 4, 5, 6, 8, ...</pre>
</section>
## Instructions
<section id='instructions'>
Create two functions named `ffr` and `ffs` that when given `n` return `R(n)` or `S(n)` respectively. (Note that R(1) = 1 and S(1) = 2 to avoid off-by-one errors).
No maximum value for `n` should be assumed.
**References**
<ul>
<li>
Sloane's <a href='https://oeis.org/A005228' target='_blank'>A005228</a> and <a href='https://oeis.org/A030124' target='_blank'>A030124</a>.
</li>
<li>
Wikipedia: <a href='https://en.wikipedia.org/wiki/Hofstadter_sequence#Hofstadter_Figure-Figure_sequences' title='wp: Hofstadter_sequence#Hofstadter_Figure-Figure_sequences' target='_blank'>Hofstadter Figure-Figure sequences</a>.
</li>
</ul>
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: <code>ffr</code> should be a function.
testString: assert(typeof ffr === 'function');
- text: <code>ffs</code> should be a function.
testString: assert(typeof ffs === 'function');
- text: <code>ffr</code> should return integer.
testString: assert(Number.isInteger(ffr(1)));
- text: <code>ffs</code> should return integer.
testString: assert(Number.isInteger(ffs(1)));
- text: <code>ffr()</code> should return <code>69</code>
testString: assert.equal(ffr(ffrParamRes[0][0]), ffrParamRes[0][1]);
- text: <code>ffr()</code> should return <code>1509</code>
testString: assert.equal(ffr(ffrParamRes[1][0]), ffrParamRes[1][1]);
- text: <code>ffr()</code> should return <code>5764</code>
testString: assert.equal(ffr(ffrParamRes[2][0]), ffrParamRes[2][1]);
- text: <code>ffr()</code> should return <code>526334</code>
testString: assert.equal(ffr(ffrParamRes[3][0]), ffrParamRes[3][1]);
- text: <code>ffs()</code> should return <code>14</code>
testString: assert.equal(ffs(ffsParamRes[0][0]), ffsParamRes[0][1]);
- text: <code>ffs()</code> should return <code>59</code>
testString: assert.equal(ffs(ffsParamRes[1][0]), ffsParamRes[1][1]);
- text: <code>ffs()</code> should return <code>112</code>
testString: assert.equal(ffs(ffsParamRes[2][0]), ffsParamRes[2][1]);
- text: <code>ffs()</code> should return <code>1041</code>
testString: assert.equal(ffs(ffsParamRes[3][0]), ffsParamRes[3][1]);
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='js-seed'>
```js
// noprotect
function ffr(n) {
return n;
}
function ffs(n) {
return n;
}
```
</div>
### After Test
<div id='js-teardown'>
```js
const ffrParamRes = [[10, 69], [50, 1509], [100, 5764], [1000, 526334]];
const ffsParamRes = [[10, 14], [50, 59], [100, 112], [1000, 1041]];
```
</div>
</section>
## Solution
<section id='solution'>
```js
// noprotect
const R = [null, 1];
const S = [null, 2];
function extendSequences (n) {
let current = Math.max(R[R.length - 1], S[S.length - 1]);
let i;
while (R.length <= n || S.length <= n) {
i = Math.min(R.length, S.length) - 1;
current += 1;
if (current === R[i] + S[i]) {
R.push(current);
} else {
S.push(current);
}
}
}
function ffr (n) {
extendSequences(n);
return R[n];
}
function ffs (n) {
extendSequences(n);
return S[n];
}
```
</section>

View File

@ -0,0 +1,77 @@
---
id: bad87fee1348bd9aedf08816
title: Link to External Pages with Anchor Elements
challengeType: 0
videoUrl: 'https://scrimba.com/p/pVMPUv/c8EkncB'
forumTopicId: 18226
---
## Description
<section id='description'>
You can use <code>a</code> (<i>anchor</i>) elements to link to content outside of your web page.
<code>a</code> elements need a destination web address called an <code>href</code> attribute. They also need anchor text. Here's an example:
EXAMPLE REMOVED AS IT'S NESTED
Then your browser will display the text <strong>"this links to freecodecamp.org"</strong> as a link you can click. And that link will take you to the web address <strong>https://www.freecodecamp.org</strong>.
</section>
## Instructions
<section id='instructions'>
Create an <code>a</code> element that links to <code>http://freecatphotoapp.com</code> and has "cat photos" as its anchor text.
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: Your <code>a</code> element should have the anchor text of "cat photos".
testString: assert((/cat photos/gi).test($("a").text()));
- text: You need an <code>a</code> element that links to <code>http&#58;//freecatphotoapp<wbr>.com</code>
testString: assert(/http:\/\/(www\.)?freecatphotoapp\.com/gi.test($("a").attr("href")));
- text: Make sure your <code>a</code> element has a closing tag.
testString: assert(code.match(/<\/a>/g) && code.match(/<\/a>/g).length === code.match(/<a/g).length);
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='html-seed'>
```html
<h2>CatPhotoApp</h2>
<main>
<img src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back.">
<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
<p>Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.</p>
</main>
```
</div>
</section>
## Solution
<section id='solution'>
```html
<h2>CatPhotoApp</h2>
<main>
<img src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back.">
<a href="http://freecatphotoapp.com">cat photos</a>
<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
<p>Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.</p>
</main>
```
</section>

View File

@ -0,0 +1,87 @@
---
id: bad87fee1348bd9aedf08816
title: Link to External Pages with Anchor Elements
challengeType: 0
videoUrl: 'https://scrimba.com/p/pVMPUv/c8EkncB'
forumTopicId: 18226
---
## Description
<section id='description'>
You can use `a` (*anchor*) elements to link to content outside of your web page.
`a` elements need a destination web address called an `href` attribute. They also need anchor text. Here's an example:
EXAMPLE REMOVED AS IT'S NESTED
Then your browser will display the text **"this links to freecodecamp.org"** as a link you can click. And that link will take you to the web address **`https://www.freecodecamp.org`**.
</section>
## Instructions
<section id='instructions'>
Create an `a` element that links to `http://freecatphotoapp.com` and has "cat photos" as its anchor text.
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: Your <code>a</code> element should have the anchor text of "cat photos".
testString: assert((/cat photos/gi).test($("a").text()));
- text: You need an <code>a</code> element that links to <code>http&#58;//freecatphotoapp<wbr>.com</code>
testString: assert(/http:\/\/(www\.)?freecatphotoapp\.com/gi.test($("a").attr("href")));
- text: Make sure your <code>a</code> element has a closing tag.
testString: assert(code.match(/<\/a>/g) && code.match(/<\/a>/g).length === code.match(/<a/g).length);
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='html-seed'>
```html
<h2>CatPhotoApp</h2>
<main>
<img src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back.">
<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
<p>Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.</p>
</main>
```
</div>
</section>
## Solution
<section id='solution'>
```html
<h2>CatPhotoApp</h2>
<main>
<img src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back.">
<a href="http://freecatphotoapp.com">cat photos</a>
<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
<p>Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.</p>
</main>
```
</section>

View File

@ -0,0 +1,101 @@
---
id: bad88fee1348bd9aedf08816
title: Link to Internal Sections of a Page with Anchor Elements
challengeType: 0
videoUrl: 'https://scrimba.com/p/pVMPUv/cyrDRUL'
forumTopicId: 301098
---
## Description
<section id='description'>
<code>a</code> (<i>anchor</i>) elements can also be used to create internal links to jump to different sections within a webpage.
To create an internal link, you assign a link's <code>href</code> attribute to a hash symbol <code>#</code> plus the value of the <code>id</code> attribute for the element that you want to internally link to, usually further down the page. You then need to add the same <code>id</code> attribute to the element you are linking to. An <code>id</code> is an attribute that uniquely describes an element.
Below is an example of an internal anchor link and its target element:
```html
<a href="#contacts-header">Contacts</a>
...
<h2 id="contacts-header">Contacts</h2>
```
When users click the Contacts link, they'll be taken to the section of the webpage with the <b>Contacts</b> header element.
</section>
## Instructions
<section id='instructions'>
Change your external link to an internal link by changing the <code>href</code> attribute to "#footer" and the text from "cat photos" to "Jump to Bottom".
Remove the <code>target="_blank"</code> attribute from the anchor tag since this causes the linked document to open in a new window tab.
Then add an <code>id</code> attribute with a value of "footer" to the <code>&lt;footer&gt;</code> element at the bottom of the page.
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: There should be only one anchor tag on your page.
testString: assert($('a').length == 1);
- text: There should be only one <code>footer</code> tag on your page.
testString: assert($('footer').length == 1);
- text: The <code>a</code> tag should have an <code>href</code> attribute set to "#footer".
testString: assert($('a').eq(0).attr('href') == "#footer");
- text: The <code>a</code> tag should not have a <code>target</code> attribute
testString: assert(typeof $('a').eq(0).attr('target') == typeof undefined || $('a').eq(0).attr('target') == true);
- text: The <code>a</code> text should be "Jump to Bottom".
testString: assert($('a').eq(0).text().match(/Jump to Bottom/gi));
- text: The <code>footer</code> tag should have an <code>id</code> attribute set to "footer".
testString: assert($('footer').eq(0).attr('id') == "footer");
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='html-seed'>
```html
<h2>CatPhotoApp</h2>
<main>
<a href="http://freecatphotoapp.com" target="_blank">cat photos</a>
<img src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back.">
<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff. Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched. Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
<p>Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched. Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff. Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.</p>
<p>Meowwww loved it, hated it, loved it, hated it yet spill litter box, scratch at owner, destroy all furniture, especially couch or lay on arms while you're using the keyboard. Missing until dinner time toy mouse squeak roll over. With tail in the air lounge in doorway. Man running from cops stops to pet cats, goes to jail.</p>
<p>Intently stare at the same spot poop in the plant pot but kitten is playing with dead mouse. Get video posted to internet for chasing red dot leave fur on owners clothes meow to be let out and mesmerizing birds leave fur on owners clothes or favor packaging over toy so purr for no reason. Meow to be let out play time intently sniff hand run outside as soon as door open yet destroy couch.</p>
</main>
<footer>Copyright Cat Photo App</footer>
```
</div>
</section>
## Solution
<section id='solution'>
```html
<h2>CatPhotoApp</h2>
<main>
<a href="#footer">Jump to Bottom</a>
<img src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back.">
<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff. Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched. Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
<p>Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched. Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff. Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.</p>
<p>Meowwww loved it, hated it, loved it, hated it yet spill litter box, scratch at owner, destroy all furniture, especially couch or lay on arms while you're using the keyboard. Missing until dinner time toy mouse squeak roll over. With tail in the air lounge in doorway. Man running from cops stops to pet cats, goes to jail.</p>
<p>Intently stare at the same spot poop in the plant pot but kitten is playing with dead mouse. Get video posted to internet for chasing red dot leave fur on owners clothes meow to be let out and mesmerizing birds leave fur on owners clothes or favor packaging over toy so purr for no reason. Meow to be let out play time intently sniff hand run outside as soon as door open yet destroy couch.</p>
</main>
<footer id="footer">Copyright Cat Photo App</footer>
```
</section>

View File

@ -0,0 +1,114 @@
---
id: bad88fee1348bd9aedf08816
title: Link to Internal Sections of a Page with Anchor Elements
challengeType: 0
videoUrl: 'https://scrimba.com/p/pVMPUv/cyrDRUL'
forumTopicId: 301098
---
## Description
<section id='description'>
`a` (*anchor*) elements can also be used to create internal links to jump to different sections within a webpage.
To create an internal link, you assign a link's `href` attribute to a hash symbol `#` plus the value of the `id` attribute for the element that you want to internally link to, usually further down the page. You then need to add the same `id` attribute to the element you are linking to. An `id` is an attribute that uniquely describes an element.
Below is an example of an internal anchor link and its target element:
```html
<a href="#contacts-header">Contacts</a>
...
<h2 id="contacts-header">Contacts</h2>
```
When users click the Contacts link, they'll be taken to the section of the webpage with the **Contacts** header element.
</section>
## Instructions
<section id='instructions'>
Change your external link to an internal link by changing the `href` attribute to "#footer" and the text from "cat photos" to "Jump to Bottom".
Remove the `target="_blank"` attribute from the anchor tag since this causes the linked document to open in a new window tab.
Then add an `id` attribute with a value of "footer" to the `<footer>` element at the bottom of the page.
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: There should be only one anchor tag on your page.
testString: assert($('a').length == 1);
- text: There should be only one <code>footer</code> tag on your page.
testString: assert($('footer').length == 1);
- text: The <code>a</code> tag should have an <code>href</code> attribute set to "#footer".
testString: assert($('a').eq(0).attr('href') == "#footer");
- text: The <code>a</code> tag should not have a <code>target</code> attribute
testString: assert(typeof $('a').eq(0).attr('target') == typeof undefined || $('a').eq(0).attr('target') == true);
- text: The <code>a</code> text should be "Jump to Bottom".
testString: assert($('a').eq(0).text().match(/Jump to Bottom/gi));
- text: The <code>footer</code> tag should have an <code>id</code> attribute set to "footer".
testString: assert($('footer').eq(0).attr('id') == "footer");
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='html-seed'>
```html
<h2>CatPhotoApp</h2>
<main>
<a href="http://freecatphotoapp.com" target="_blank">cat photos</a>
<img src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back.">
<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff. Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched. Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
<p>Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched. Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff. Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.</p>
<p>Meowwww loved it, hated it, loved it, hated it yet spill litter box, scratch at owner, destroy all furniture, especially couch or lay on arms while you're using the keyboard. Missing until dinner time toy mouse squeak roll over. With tail in the air lounge in doorway. Man running from cops stops to pet cats, goes to jail.</p>
<p>Intently stare at the same spot poop in the plant pot but kitten is playing with dead mouse. Get video posted to internet for chasing red dot leave fur on owners clothes meow to be let out and mesmerizing birds leave fur on owners clothes or favor packaging over toy so purr for no reason. Meow to be let out play time intently sniff hand run outside as soon as door open yet destroy couch.</p>
</main>
<footer>Copyright Cat Photo App</footer>
```
</div>
</section>
## Solution
<section id='solution'>
```html
<h2>CatPhotoApp</h2>
<main>
<a href="#footer">Jump to Bottom</a>
<img src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back.">
<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff. Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched. Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
<p>Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched. Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff. Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.</p>
<p>Meowwww loved it, hated it, loved it, hated it yet spill litter box, scratch at owner, destroy all furniture, especially couch or lay on arms while you're using the keyboard. Missing until dinner time toy mouse squeak roll over. With tail in the air lounge in doorway. Man running from cops stops to pet cats, goes to jail.</p>
<p>Intently stare at the same spot poop in the plant pot but kitten is playing with dead mouse. Get video posted to internet for chasing red dot leave fur on owners clothes meow to be let out and mesmerizing birds leave fur on owners clothes or favor packaging over toy so purr for no reason. Meow to be let out play time intently sniff hand run outside as soon as door open yet destroy couch.</p>
</main>
<footer id="footer">Copyright Cat Photo App</footer>
```
</section>

View File

@ -0,0 +1,100 @@
---
id: bad87fee1348bd9aede08817
title: Nest an Anchor Element within a Paragraph
challengeType: 0
videoUrl: 'https://scrimba.com/p/pVMPUv/cb6k8Cb'
forumTopicId: 18244
---
## Description
<section id='description'>
You can nest links within other text elements.
```html
<p>
Here's a <a target="_blank" href="http://freecodecamp.org"> link to freecodecamp.org</a> for you to follow.
</p>
```
Let's break down the example:
Normal text is wrapped in the <code>p</code> element:<br> <code>&#60;p&#62; Here's a ... for you to follow. &#60;/p&#62;</code>
Next is the <i>anchor</i> element <code>&#60;a&#62;</code> (which requires a closing tag <code>&#60;/a&#62;</code>):<br> <code>&#60;a&#62; ... &#60;/a&#62;</code>
<code>target</code> is an anchor tag attribute that specifies where to open the link and the value <code>"_blank"</code> specifies to open the link in a new tab
<code>href</code> is an anchor tag attribute that contains the URL address of the link:<br> `<a href="http://freecodecamp.org"> ... </a>`
The text, <strong>"link to freecodecamp.org"</strong>, within the <code>a</code> element called <code>anchor text</code>, will display a link to click:<br> <code>&#60;a href=" ... "&#62;link to freecodecamp.org&#60;/a&#62;</code>
The final output of the example will look like this:<br><p>Here's a <a target="_blank" href="http://freecodecamp.org"> link to freecodecamp.org</a> for you to follow.</p>
</section>
## Instructions
<section id='instructions'>
Now nest the existing <code>a</code> element within a new <code>p</code> element (just after the existing <code>main</code> element). The new paragraph should have text that says "View more cat photos", where "cat photos" is a link, and the rest of the text is plain text.
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: You need an <code>a</code> element that links to "http://freecatphotoapp.com".
testString: assert(($("a[href=\"http://freecatphotoapp.com\"]").length > 0 || $("a[href=\"http://www.freecatphotoapp.com\"]").length > 0));
- text: Your <code>a</code> element should have the anchor text of "cat photos"
testString: assert($("a").text().match(/cat\sphotos/gi));
- text: Create a new <code>p</code> element around your <code>a</code> element. There should be at least 3 total <code>p</code> tags in your HTML code.
testString: assert($("p") && $("p").length > 2);
- text: Your <code>a</code> element should be nested within your new <code>p</code> element.
testString: assert(($("a[href=\"http://freecatphotoapp.com\"]").parent().is("p") || $("a[href=\"http://www.freecatphotoapp.com\"]").parent().is("p")));
- text: Your <code>p</code> element should have the text "View more " (with a space after it).
testString: assert(($("a[href=\"http://freecatphotoapp.com\"]").parent().text().match(/View\smore\s/gi) || $("a[href=\"http://www.freecatphotoapp.com\"]").parent().text().match(/View\smore\s/gi)));
- text: Your <code>a</code> element should <em>not</em> have the text "View more".
testString: assert(!$("a").text().match(/View\smore/gi));
- text: Make sure each of your <code>p</code> elements has a closing tag.
testString: assert(code.match(/<\/p>/g) && code.match(/<p/g) && code.match(/<\/p>/g).length === code.match(/<p/g).length);
- text: Make sure each of your <code>a</code> elements has a closing tag.
testString: assert(code.match(/<\/a>/g) && code.match(/<a/g) && code.match(/<\/a>/g).length === code.match(/<a/g).length);
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='html-seed'>
```html
<h2>CatPhotoApp</h2>
<main>
<a href="http://freecatphotoapp.com" target="_blank">cat photos</a>
<img src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back.">
<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
<p>Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.</p>
</main>
```
</div>
</section>
## Solution
<section id='solution'>
```html
<h2>CatPhotoApp</h2>
<main>
<p>View more <a target="_blank" href="http://freecatphotoapp.com">cat photos</a></p>
<img src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back.">
<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
<p>Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.</p>
</main>
```
</section>

View File

@ -0,0 +1,105 @@
---
id: bad87fee1348bd9aede08817
title: Nest an Anchor Element within a Paragraph
challengeType: 0
videoUrl: 'https://scrimba.com/p/pVMPUv/cb6k8Cb'
forumTopicId: 18244
---
## Description
<section id='description'>
You can nest links within other text elements.
```html
<p>
Here's a <a target="_blank" href="http://freecodecamp.org"> link to freecodecamp.org</a> for you to follow.
</p>
```
Let's break down the example: Normal text is wrapped in the `p` element:
`<p> Here's a ... for you to follow. </p>` Next is the *anchor* element `<a>` (which requires a closing tag `</a>`):
`<a> ... </a>` `target` is an anchor tag attribute that specifies where to open the link and the value `"_blank"` specifies to open the link in a new tab `href` is an anchor tag attribute that contains the URL address of the link:
`<a href="http://freecodecamp.org"> ... </a>` The text, **"link to freecodecamp.org"**, within the `a` element called `anchor text`, will display a link to click:
`<a href=" ... ">link to freecodecamp.org</a>` The final output of the example will look like this:
Here's a [link to freecodecamp.org](http://freecodecamp.org) for you to follow.
</section>
## Instructions
<section id='instructions'>
Now nest the existing `a` element within a new `p` element (just after the existing `main` element). The new paragraph should have text that says "View more cat photos", where "cat photos" is a link, and the rest of the text is plain text.
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: You need an <code>a</code> element that links to "http://freecatphotoapp.com".
testString: assert(($("a[href=\"http://freecatphotoapp.com\"]").length > 0 || $("a[href=\"http://www.freecatphotoapp.com\"]").length > 0));
- text: Your <code>a</code> element should have the anchor text of "cat photos"
testString: assert($("a").text().match(/cat\sphotos/gi));
- text: Create a new <code>p</code> element around your <code>a</code> element. There should be at least 3 total <code>p</code> tags in your HTML code.
testString: assert($("p") && $("p").length > 2);
- text: Your <code>a</code> element should be nested within your new <code>p</code> element.
testString: assert(($("a[href=\"http://freecatphotoapp.com\"]").parent().is("p") || $("a[href=\"http://www.freecatphotoapp.com\"]").parent().is("p")));
- text: Your <code>p</code> element should have the text "View more " (with a space after it).
testString: assert(($("a[href=\"http://freecatphotoapp.com\"]").parent().text().match(/View\smore\s/gi) || $("a[href=\"http://www.freecatphotoapp.com\"]").parent().text().match(/View\smore\s/gi)));
- text: Your <code>a</code> element should <em>not</em> have the text "View more".
testString: assert(!$("a").text().match(/View\smore/gi));
- text: Make sure each of your <code>p</code> elements has a closing tag.
testString: assert(code.match(/<\/p>/g) && code.match(/<p/g) && code.match(/<\/p>/g).length === code.match(/<p/g).length);
- text: Make sure each of your <code>a</code> elements has a closing tag.
testString: assert(code.match(/<\/a>/g) && code.match(/<a/g) && code.match(/<\/a>/g).length === code.match(/<a/g).length);
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='html-seed'>
```html
<h2>CatPhotoApp</h2>
<main>
<a href="http://freecatphotoapp.com" target="_blank">cat photos</a>
<img src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back.">
<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
<p>Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.</p>
</main>
```
</div>
</section>
## Solution
<section id='solution'>
```html
<h2>CatPhotoApp</h2>
<main>
<p>View more <a target="_blank" href="http://freecatphotoapp.com">cat photos</a></p>
<img src="https://bit.ly/fcc-relaxing-cat" alt="A cute orange cat lying on its back.">
<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
<p>Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.</p>
</main>
```
</section>

View File

@ -0,0 +1,84 @@
---
id: 5a23c84252665b21eecc801c
title: Spiral matrix
challengeType: 5
forumTopicId: 302321
---
## Description
<section id='description'>
Produce a spiral array.
A <i>spiral array</i> is a square arrangement of the first N<sup>2</sup> natural numbers, where the numbers increase sequentially as you go around the edges of the array spiraling inwards.
For example, given <b>5</b>, produce this array:
<pre>
0 1 2 3 4
15 16 17 18 5
14 23 24 19 6
13 22 21 20 7
12 11 10 9 8
</pre>
</section>
## Instructions
<section id='instructions'>
</section>
## Tests
<section id='tests'>
``` yml
tests:
- text: <code>spiralArray</code> should be a function.
testString: assert(typeof spiralArray=='function','<code>spiralArray</code> should be a function.');
- text: <code>spiralArray(3)</code> should return an array.
testString: assert(Array.isArray(spiralArray(3)), '<code>spiralArray(3)</code> should return an array.');
- text: <code>spiralArray(3)</code> should return <code>[[0, 1, 2],[7, 8, 3],[6, 5, 4]]</code>.
testString: assert.deepEqual(spiralArray(3), [[0, 1, 2], [7, 8, 3], [6, 5, 4]], '<code>spiralArray(3)</code> should return <code>[[0, 1, 2],[7, 8, 3],[6, 5, 4]]</code>.');
- text: <code>spiralArray(4)</code> should return <code>[[0, 1, 2, 3],[11, 12, 13, 4],[10, 15, 14, 5],[9, 8, 7, 6]]</code>.
testString: assert.deepEqual(spiralArray(4), [[0, 1, 2, 3], [11, 12, 13, 4], [10, 15, 14, 5], [9, 8, 7, 6]], '<code>spiralArray(4)</code> should return <code>[[0, 1, 2, 3],[11, 12, 13, 4],[10, 15, 14, 5],[9, 8, 7, 6]]</code>.');
- text: <code>spiralArray(5)</code> should return <code>[[0, 1, 2, 3, 4],[15, 16, 17, 18, 5],[14, 23, 24, 19, 6],[13, 22, 21, 20, 7],[12, 11, 10, 9, 8]]</code>.
testString: assert.deepEqual(spiralArray(5), [[0, 1, 2, 3, 4], [15, 16, 17, 18, 5], [14, 23, 24, 19, 6], [13, 22, 21, 20, 7], [12, 11, 10, 9, 8]], '<code>spiralArray(5)</code> should return <code>[[0, 1, 2, 3, 4],[15, 16, 17, 18, 5],[14, 23, 24, 19, 6],[13, 22, 21, 20, 7],[12, 11, 10, 9, 8]]</code>.');
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='js-seed'>
```js
function spiralArray(n) {
// Good luck!
}
```
</div>
</section>
## Solution
<section id='solution'>
```js
function spiralArray(n) {
var arr = Array(n),
x = 0, y = n,
total = n * n--,
dx = 1, dy = 0,
i = 0, j = 0;
while (y) arr[--y] = [];
while (i < total) {
arr[y][x] = i++;
x += dx; y += dy;
if (++j == n) {
if (dy < 0) {x++; y++; n -= 2}
j = dx; dx = -dy; dy = j; j = 0;
}
}
return arr;
}
```
</section>

View File

@ -0,0 +1,90 @@
---
id: 5a23c84252665b21eecc801c
title: Spiral matrix
challengeType: 5
forumTopicId: 302321
---
## Description
<section id='description'>
Produce a spiral array. A *spiral array* is a square arrangement of the first N<sup>2</sup> natural numbers, where the numbers increase sequentially as you go around the edges of the array spiraling inwards. For example, given **5**, produce this array:
<pre>
0 1 2 3 4
15 16 17 18 5
14 23 24 19 6
13 22 21 20 7
12 11 10 9 8
</pre>
</section>
## Instructions
<section id='instructions'>
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: <code>spiralArray</code> should be a function.
testString: assert(typeof spiralArray=='function','<code>spiralArray</code> should be a function.');
- text: <code>spiralArray(3)</code> should return an array.
testString: assert(Array.isArray(spiralArray(3)), '<code>spiralArray(3)</code> should return an array.');
- text: <code>spiralArray(3)</code> should return <code>[[0, 1, 2],[7, 8, 3],[6, 5, 4]]</code>.
testString: assert.deepEqual(spiralArray(3), [[0, 1, 2], [7, 8, 3], [6, 5, 4]], '<code>spiralArray(3)</code> should return <code>[[0, 1, 2],[7, 8, 3],[6, 5, 4]]</code>.');
- text: <code>spiralArray(4)</code> should return <code>[[0, 1, 2, 3],[11, 12, 13, 4],[10, 15, 14, 5],[9, 8, 7, 6]]</code>.
testString: assert.deepEqual(spiralArray(4), [[0, 1, 2, 3], [11, 12, 13, 4], [10, 15, 14, 5], [9, 8, 7, 6]], '<code>spiralArray(4)</code> should return <code>[[0, 1, 2, 3],[11, 12, 13, 4],[10, 15, 14, 5],[9, 8, 7, 6]]</code>.');
- text: <code>spiralArray(5)</code> should return <code>[[0, 1, 2, 3, 4],[15, 16, 17, 18, 5],[14, 23, 24, 19, 6],[13, 22, 21, 20, 7],[12, 11, 10, 9, 8]]</code>.
testString: assert.deepEqual(spiralArray(5), [[0, 1, 2, 3, 4], [15, 16, 17, 18, 5], [14, 23, 24, 19, 6], [13, 22, 21, 20, 7], [12, 11, 10, 9, 8]], '<code>spiralArray(5)</code> should return <code>[[0, 1, 2, 3, 4],[15, 16, 17, 18, 5],[14, 23, 24, 19, 6],[13, 22, 21, 20, 7],[12, 11, 10, 9, 8]]</code>.');
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='js-seed'>
```js
function spiralArray(n) {
// Good luck!
}
```
</div>
</section>
## Solution
<section id='solution'>
```js
function spiralArray(n) {
var arr = Array(n),
x = 0, y = n,
total = n * n--,
dx = 1, dy = 0,
i = 0, j = 0;
while (y) arr[--y] = [];
while (i < total) {
arr[y][x] = i++;
x += dx; y += dy;
if (++j == n) {
if (dy < 0) {x++; y++; n -= 2}
j = dx; dx = -dy; dy = j; j = 0;
}
}
return arr;
}
```
</section>

View File

@ -0,0 +1,91 @@
---
id: 56533eb9ac21ba0edf2244bb
title: Word Blanks
challengeType: 1
videoUrl: 'https://scrimba.com/c/caqn8zuP'
forumTopicId: 18377
---
## Description
<section id='description'>
We will now use our knowledge of strings to build a "<a href='https://en.wikipedia.org/wiki/Mad_Libs' target='_blank'>Mad Libs</a>" style word game we're calling "Word Blanks". You will create an (optionally humorous) "Fill in the Blanks" style sentence.
In a "Mad Libs" game, you are provided sentences with some missing words, like nouns, verbs, adjectives and adverbs. You then fill in the missing pieces with words of your choice in a way that the completed sentence makes sense.
Consider this sentence - "It was really <strong>____</strong>, and we <strong>____</strong> ourselves <strong>____</strong>". This sentence has three missing pieces- an adjective, a verb and an adverb, and we can add words of our choice to complete it. We can then assign the completed sentence to a variable as follows:
```js
var sentence = "It was really " + "hot" + ", and we " + "laughed" + " ourselves " + "silly" + ".";
```
</section>
## Instructions
<section id='instructions'>
In this challenge, we provide you with a noun, a verb, an adjective and an adverb. You need to form a complete sentence using words of your choice, along with the words we provide.
You will need to use the string concatenation operator <code>+</code> to build a new string, using the provided variables: <code>myNoun</code>, <code>myAdjective</code>, <code>myVerb</code>, and <code>myAdverb</code>. You will then assign the formed string to the <code>wordBlanks</code> variable. You should not change the words assigned to the variables.
You will also need to account for spaces in your string, so that the final sentence has spaces between all the words. The result should be a complete sentence.
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: <code>wordBlanks</code> should be a string.
testString: assert(typeof wordBlanks === 'string');
- text: You should not change the values assigned to <code>myNoun</code>, <code>myVerb</code>, <code>myAdjective</code> or <code>myAdverb</code>.
testString: assert(myNoun === "dog" && myVerb === "ran" && myAdjective === "big" && myAdverb === "quickly");
- text: You should not directly use the values "dog", "ran", "big", or "quickly" to create <code>wordBlanks</code>.
testString: const newCode = removeAssignments(code); assert(!/dog/.test(newCode) && !/ran/.test(newCode) && !/big/.test(newCode) && !/quickly/.test(newCode));
- text: <code>wordBlanks</code> should contain all of the words assigned to the variables <code>myNoun</code>, <code>myVerb</code>, <code>myAdjective</code> and <code>myAdverb</code> separated by non-word characters (and any additional words in your madlib).
testString: assert(/\bdog\b/.test(wordBlanks) && /\bbig\b/.test(wordBlanks) && /\bran\b/.test(wordBlanks) && /\bquickly\b/.test(wordBlanks));
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='js-seed'>
```js
var myNoun = "dog";
var myAdjective = "big";
var myVerb = "ran";
var myAdverb = "quickly";
var wordBlanks = ""; // Only change this line;
```
</div>
<div id='js-teardown'>
```js
const removeAssignments = str => str
.replace(/myNoun\s*=\s*["']dog["']/g, '')
.replace(/myAdjective\s*=\s*["']big["']/g, '')
.replace(/myVerb\s*=\s*["']ran["']/g, '')
.replace(/myAdverb\s*=\s*["']quickly["']/g, '');
```
</div>
</section>
## Solution
<section id='solution'>
```js
var myNoun = "dog";
var myAdjective = "big";
var myVerb = "ran";
var myAdverb = "quickly";
var wordBlanks = "Once there was a " + myNoun + " which was very " + myAdjective + ". ";
wordBlanks += "It " + myVerb + " " + myAdverb + " around the yard.";
```
</section>

View File

@ -0,0 +1,102 @@
---
id: 56533eb9ac21ba0edf2244bb
title: Word Blanks
challengeType: 1
videoUrl: 'https://scrimba.com/c/caqn8zuP'
forumTopicId: 18377
---
## Description
<section id='description'>
We will now use our knowledge of strings to build a "[Mad Libs](https://en.wikipedia.org/wiki/Mad_Libs)" style word game we're calling "Word Blanks". You will create an (optionally humorous) "Fill in the Blanks" style sentence.
In a "Mad Libs" game, you are provided sentences with some missing words, like nouns, verbs, adjectives and adverbs. You then fill in the missing pieces with words of your choice in a way that the completed sentence makes sense.
Consider this sentence - "It was really **\_\_\_\_**, and we **\_\_\_\_** ourselves **\_\_\_\_**". This sentence has three missing pieces- an adjective, a verb and an adverb, and we can add words of our choice to complete it. We can then assign the completed sentence to a variable as follows:
```js
var sentence = "It was really " + "hot" + ", and we " + "laughed" + " ourselves " + "silly" + ".";
```
</section>
## Instructions
<section id='instructions'>
In this challenge, we provide you with a noun, a verb, an adjective and an adverb. You need to form a complete sentence using words of your choice, along with the words we provide.
You will need to use the string concatenation operator `+` to build a new string, using the provided variables: `myNoun`, `myAdjective`, `myVerb`, and `myAdverb`. You will then assign the formed string to the `wordBlanks` variable. You should not change the words assigned to the variables.
You will also need to account for spaces in your string, so that the final sentence has spaces between all the words. The result should be a complete sentence.
</section>
## Tests
<section id='tests'>
```yml
tests:
- text: <code>wordBlanks</code> should be a string.
testString: assert(typeof wordBlanks === 'string');
- text: You should not change the values assigned to <code>myNoun</code>, <code>myVerb</code>, <code>myAdjective</code> or <code>myAdverb</code>.
testString: assert(myNoun === "dog" && myVerb === "ran" && myAdjective === "big" && myAdverb === "quickly");
- text: You should not directly use the values "dog", "ran", "big", or "quickly" to create <code>wordBlanks</code>.
testString: const newCode = removeAssignments(code); assert(!/dog/.test(newCode) && !/ran/.test(newCode) && !/big/.test(newCode) && !/quickly/.test(newCode));
- text: <code>wordBlanks</code> should contain all of the words assigned to the variables <code>myNoun</code>, <code>myVerb</code>, <code>myAdjective</code> and <code>myAdverb</code> separated by non-word characters (and any additional words in your madlib).
testString: assert(/\bdog\b/.test(wordBlanks) && /\bbig\b/.test(wordBlanks) && /\bran\b/.test(wordBlanks) && /\bquickly\b/.test(wordBlanks));
```
</section>
## Challenge Seed
<section id='challengeSeed'>
<div id='js-seed'>
```js
var myNoun = "dog";
var myAdjective = "big";
var myVerb = "ran";
var myAdverb = "quickly";
var wordBlanks = ""; // Only change this line;
```
</div>
<div id='js-teardown'>
```js
const removeAssignments = str => str
.replace(/myNoun\s*=\s*["']dog["']/g, '')
.replace(/myAdjective\s*=\s*["']big["']/g, '')
.replace(/myVerb\s*=\s*["']ran["']/g, '')
.replace(/myAdverb\s*=\s*["']quickly["']/g, '');
```
</div>
</section>
## Solution
<section id='solution'>
```js
var myNoun = "dog";
var myAdjective = "big";
var myVerb = "ran";
var myAdverb = "quickly";
var wordBlanks = "Once there was a " + myNoun + " which was very " + myAdjective + ". ";
wordBlanks += "It " + myVerb + " " + myAdverb + " around the yard.";
```
</section>

View File

@ -0,0 +1,65 @@
/* global expect */
const fs = require('fs');
const path = require('path');
const {
insertSpaces,
codeToBackticks,
prettify
} = require('./transformChallenges');
// NOTE: As far as html rendering is concerned, it doesn't matter if you write
/*
<pre> two spaces
</pre>
*/
// or
/*
<pre>
two spaces
</pre>
*/
// so the html parser trims any leading spaces.
const fixtures = [
'amicable-pairs.md',
'entropy.md',
'dead-links.md',
'nest-anchor.md',
'hello.md',
'billion-names.md',
'link-internal.md',
'link-external.md',
'drum-machine.md',
'word-blanks.md',
'css.md',
'disjoint-sublist.md',
'spiral-matrix.md',
'bootstrap-block-button.md',
'hofstadter.md'
];
describe('Challenge formatter', () => {
fixtures.forEach(fixture =>
it(`should transform ${fixture} into GFM correctly`, () => {
return insertSpaces(
path.resolve(__dirname, '__fixtures__/' + fixture),
true
)
.then(codeToBackticks)
.then(prettify)
.then(output => {
const formattedMd = fs.readFileSync(
path.resolve(__dirname, '__fixtures__/' + fixture + '.formatted'),
{
encoding: 'utf8'
}
);
expect(output).toEqual(formattedMd);
});
})
);
});

View File

@ -0,0 +1,88 @@
const visit = require('unist-util-visit');
const toHast = require('mdast-util-to-hast');
const raw = require('hast-util-raw');
const toMdast = require('hast-util-to-mdast');
const toHtml = require('hast-util-to-html');
const inlineCode = require('hast-util-to-mdast/lib/handlers/inline-code');
function getCodeToInline(shouldThrow) {
return (h, node) => {
if (node.children.length > 1) {
if (shouldThrow) {
console.log('Leaving code block as it does not just contain text');
console.log(node);
throw Error('Too many children');
} else {
return rawHtml(h, node);
}
} else {
return inlineCode(h, node);
}
};
}
function rawHtml(h, node) {
return {
type: 'html',
value: toHtml(node, {
allowDangerousCharacters: true,
allowDangerousHtml: true,
quote: "'"
})
};
}
function plugin({ shouldThrow = false, pre = false }) {
const codeToInline = getCodeToInline(shouldThrow);
return transformer;
function transformer(tree) {
if (pre) {
visit(tree, 'html', preVisitor);
} else {
visit(tree, 'paragraph', visitor);
}
function visitor(node, id, parent) {
const hast = toHast(node, { allowDangerousHtml: true });
const lastChild = hast.children.slice(-1)[0];
if (
shouldThrow &&
lastChild &&
lastChild.value &&
lastChild.value.match(/<\w*>/) &&
!lastChild.value.match(/<br>/)
) {
console.log('Unclosed tag', lastChild.value);
throw Error('Unclosed tag in paragraph.');
}
const paragraph = raw(hast);
parent.children[id] = toMdast(paragraph, {
handlers: {
code: codeToInline,
dfn: rawHtml,
sup: rawHtml,
sub: rawHtml,
button: rawHtml
}
});
}
function preVisitor(node, id, parent) {
const paragraph = raw(toHast(node, { allowDangerousHtml: true }));
parent.children[id] = toMdast(paragraph, {
handlers: {
pre: codeToInline,
dfn: rawHtml,
sup: rawHtml,
sub: rawHtml,
button: rawHtml
}
});
}
}
}
module.exports = plugin;

View File

@ -0,0 +1,21 @@
const {
insertSpaces,
codeToBackticks,
prettify
} = require('./transformChallenges');
const readDirP = require('readdirp-walk');
const fs = require('fs');
const challengeDir = '../../../../curriculum/challenges/chinese';
readDirP({ root: challengeDir, fileFilter: ['*.md'] }).on('data', file => {
if (file.stat.isFile()) {
insertSpaces(file.fullPath, true)
.then(codeToBackticks)
.then(prettify)
.then(text => fs.writeFileSync(file.fullPath, text))
.catch(() => {
console.log(file.path);
});
}
});

View File

@ -0,0 +1,257 @@
const unified = require('unified');
const visit = require('unist-util-visit');
const toHast = require('mdast-util-to-hast');
const raw = require('hast-util-raw');
const toHtml = require('hast-util-to-html');
const isEmpty = require('lodash/isEmpty');
const hastToMdast = require('hast-util-to-mdast');
const remarkStringify = require('remark-stringify');
const remarkParse = require('remark-parse');
const { text } = require('mdast-builder');
const parseEntities = require('parse-entities');
const find = require('unist-util-find');
const h = require('hastscript');
const phrasing = require('hast-util-phrasing');
const blankLine = { type: 'text', value: '\n\n' };
/* Currently the challenge parser behaves differently depending on whether a
section starts with an empty line or not. If it does not, the parser interprets
single new-line (\n) characters as paragraph breaks. It also does not parse
markdown syntax (such as `). This makes formatting challenges harder than it
needs to be, since normal markdown rules do not apply (some of the time!)
For example
<section id='instructions'>
Sentence1.
Sentence2 `var x = 'y'`.
...
becomes
Sentence1.
Sentence2 `var x = 'y'`.
in the challenge, but should become
Sentence1. Sentence2 <code>var x = 'y'</code>.
---
This file converts the instructions and descriptions. After this there will be
no need to handle the case where the first line is not empty and markdown syntax
will alway work. The linter can check that the first blank line exists.
*/
var parser = unified().use(remarkParse);
var mdCompiler = unified().use(remarkStringify);
function stringifyMd(mdast) {
return mdCompiler.stringify(mdast);
}
function parseMd(text) {
return parser.parse(text);
}
function escapeMd(hastNode) {
// defensive copy because stringify mutates objects its called on...
hastNode = { ...hastNode };
// These are added by getParagraphs so must not be touched
if (hastNode.value === '\n\n') return hastNode;
// A trailing space gets converted to \n, because hastToMdast will be told
// told this is a paragraph, which it isn't
const trailingSpace = /\s/.test(hastNode.value[hastNode.value.length - 1]);
// leading spaces also get ignored, but need to be added in, since they can
// be following html elements.
const leadingSpace = /\s/.test(hastNode.value[0]);
// fake a hast tree. Is there a less hacky approach?
const hastTree = {
type: 'root',
children: [hastNode]
};
let value = stringifyMd(hastToMdast(hastTree));
// Removing the last character is always necessary, since stringify appends \n
value = value.slice(0, -1);
if (trailingSpace) value += ' ';
if (leadingSpace) value = ' ' + value;
// convert html entities into their characters
return { type: 'text', value: parseEntities(value) };
}
function wrapBareUrls(hastNode) {
const rawText = hastNode.value;
// blank lines can't have urls.
if (/^\s*$/.test(rawText)) return null;
return wrapRecursive(rawText);
}
function wrapRecursive(rawText) {
const mdNode = parseMd(rawText);
const link = find(mdNode, { type: 'link' });
if (link) {
const url = correctUrl(link.url);
const pos = rawText.indexOf(url);
const head = rawText.slice(0, pos);
const tail = rawText.slice(pos + url.length);
return [text(head), h('code', url)].concat(wrapRecursive(tail));
} else {
return [text(rawText)];
}
}
// remark-parse is overly eager when it comes to urls, this works around that.
function correctUrl(url) {
var match = url.match(/"|'|!/);
if (match) {
return url.split(match[0])[0];
} else {
return url;
}
}
function getParagraphs(node) {
// Splitting generates unwanted expty nodes.
if (node.value === '\n') {
return blankLine;
}
let paragraphs = node.value.split('\n');
// nothing to split
if (paragraphs.length <= 1) {
return node;
}
let children = paragraphs.reduce((acc, curr) => {
return acc.concat([{ type: 'text', value: curr }, blankLine]);
}, []);
children = children.filter(({ value }) => value !== '');
// Remove the trailing newLine.
children.pop();
return children;
}
function sectionFromTemplate(section, sectionContent, closingTag) {
return (
`<section id='${section.properties.id}'>` +
'\n' +
sectionContent +
'\n' +
closingTag
);
}
function htmlVisitor(node) {
// 'html' nodes contain un-parsed html strings, so we first convert them
// to hast and then parse them to produce a syntax tree (so we can locate
// and modify the instructions and description)
const section = raw(toHast(node, { allowDangerousHtml: true }));
if (
section.type === 'element' &&
(section.properties.id === 'instructions' ||
section.properties.id === 'description') &&
!isEmpty(section.children)
) {
const hasClosingTag = /<\/section>\s*$/.test(node.value);
// section contains the section tag and all the text up to the first
// blank line.
// This replaces single line breaks with empty lines, so
// that the section text that previously required special treatment
// becomes standard markdown.
// Has to start with an empty line
section.children.unshift(blankLine);
// break the lines into paragraphs
section.children = section.children.reduce(
(acc, child) =>
acc.concat(child.type === 'text' ? getParagraphs(child) : [child]),
[]
);
// next we escape the text nodes, so that syntax like * doesn't start
// altering the formatting when it's interpretted as markdown
visit(section, (node, id, parent) => {
// no need to dive into non-phrasing nodes (once we're inside section)
// since they don't get parsed as markdown.
// An exception is made for 'big' because it is not considered to be
// phrasing, but can have markdown (that will be parsed!) inside it.
if (
node.tagName &&
node.tagName !== 'section' &&
node.tagName !== 'big' &&
!phrasing(node)
) {
return visit.SKIP;
} else if (node.type === 'text') {
parent.children[id] = escapeMd(node);
return visit.CONTINUE;
} else {
return visit.CONTINUE;
}
});
// then wrap bare urls in code tags.
visit(section, 'text', (node, id, parent) => {
// skip if it's inside an anchor element
if (parent.tagName && parent.tagName === 'a') return visit.CONTINUE;
const wrapped = wrapBareUrls(node);
if (wrapped) {
parent.children.splice(id, 1, ...wrapped);
return id + wrapped.length;
} else {
return visit.CONTINUE;
}
});
// This can come from an unclosed <section>, so we have to pretend it's
// a root element (otherwise it gets wrapped in a tag) and add the
// opening <section> back in by hand.
const sectionContent = toHtml(
{ type: 'root', children: section.children },
{
allowDangerousCharacters: true,
allowDangerousHtml: true,
quote: "'"
}
);
const closingTag = hasClosingTag ? '</section>\n' : '';
node.value = sectionFromTemplate(section, sectionContent, closingTag);
}
// If there is still a closing tag here, it should be seperated from the rest
// of the html, so that it will be parsed as a distinct html element. This
// will be important when we convert to mdx.
const hasClosingTag = /<\/section>\s*$/.test(node.value);
if (hasClosingTag) {
node.value = node.value.replace(/<\/section>\s*$/, '\n\n</section>\n');
}
}
function plugin() {
return transformer;
function transformer(tree) {
return visit(tree, 'html', htmlVisitor);
}
}
exports.insertSpaces = plugin;
exports.escapeMd = escapeMd;
exports.getParagraphs = getParagraphs;
exports.wrapBareUrls = wrapBareUrls;
exports.correctUrl = correctUrl;
exports.htmlVisitor = htmlVisitor;

View File

@ -0,0 +1,313 @@
/* global expect */
const h = require('hastscript');
const u = require('unist-builder');
const toHtml = require('hast-util-to-html');
const {
escapeMd,
getParagraphs,
htmlVisitor,
wrapBareUrls
} = require('./insert-spaces');
const blankLine = u('text', '\n\n');
describe('insert-spaces', () => {
describe('htmlVisitor', () => {
it('should separate html elements into paragraphs', () => {
/* eslint-disable max-len*/
const twoStrong = {
type: 'html',
value:
"<section id='description'>\n<strong>Example:</strong>\n<strong>1184</strong> and <strong>1210</strong> are"
};
/* eslint-enable max-len*/
htmlVisitor(twoStrong);
expect(twoStrong.value).toMatch(/<\/strong>\n\n<strong>1184<\/strong>/);
});
});
describe('getParagraphs', () => {
it('should return a node unchanged if it has no newlines', () => {
const oneLine = { type: 'text', value: 'ab' };
expect(getParagraphs(oneLine)).toEqual(oneLine);
});
it('should split a text node at a newline', () => {
const twoLines = { type: 'text', value: 'a\nb' };
expect(getParagraphs(twoLines)).toHaveLength(3);
const threeLines = { type: 'text', value: 'a\nb\nc' };
expect(getParagraphs(threeLines)).toHaveLength(5);
});
it('should create blank lines to replace \\n', () => {
const twoLines = { type: 'text', value: 'a\nb' };
const expected = [
{ type: 'text', value: 'a' },
{ type: 'text', value: '\n\n' },
{ type: 'text', value: 'b' }
];
expect(getParagraphs(twoLines)).toEqual(expected);
});
it('should replace a single \\n with a blank line', () => {
const newline = { type: 'text', value: '\n' };
const expected = { type: 'text', value: '\n\n' };
expect(getParagraphs(newline)).toEqual(expected);
});
it('should give a sentence starting \\n a starting blank line', () => {
const startingNewline = { type: 'text', value: '\na' };
const expected = [
{ type: 'text', value: '\n\n' },
{ type: 'text', value: 'a' }
];
expect(getParagraphs(startingNewline)).toEqual(expected);
});
});
describe('escapeMd', () => {
it('should not add a trailing newline', () => {
const alreadyEscaped = { type: 'text', value: 'hi!' };
expect(escapeMd(alreadyEscaped)).toEqual(alreadyEscaped);
});
it('should not escape a double newline', () => {
// they're needed to separate the paragraphs
const newLine = { type: 'text', value: '\n\n' };
expect(escapeMd(newLine)).toEqual(newLine);
});
/* This would be nice, but I don't know how to do it */
// it('should preserve newlines', () => {
// const newLine = { type: 'text', value: ' before \n after ' };
// Object.freeze(newLine);
// expect(escapeMd(newLine)).toEqual(newLine);
// });
it('should not escape urls', () => {
const url = { type: 'text', value: 'https://www.example.com' };
expect(escapeMd(url)).toEqual(url);
});
it('should escape MathJax', () => {
const mathJax = { type: 'text', value: '$H_2(X) = -\\sum_{i=1}^n$' };
const mathJaxEscaped = {
type: 'text',
value: '$H_2(X) = -\\\\sum\\_{i=1}^n$'
};
expect(escapeMd(mathJax)).toEqual(mathJaxEscaped);
});
it('should escape slashes', () => {
const mathJax = {
type: 'text',
value: '$R(1)=1\\ ;\\ S(1)=2 \\\\R(n)=R(n-1)+S(n-1), \\quad n>1.$'
};
const mathJaxEscaped = {
type: 'text',
value:
'$R(1)=1\\\\ ;\\\\ S(1)=2 \\\\\\\\R(n)=R(n-1)+S(n-1), \\\\quad n>1.$'
};
expect(escapeMd(mathJax)).toEqual(mathJaxEscaped);
});
});
describe('wrapBareUrls', () => {
it('should skip blank line nodes', () => {
expect(wrapBareUrls(blankLine)).toEqual(null);
});
it('should not modify nodes without bare urls', () => {
const noBareUrls = u('text', 'Just some words.');
const expected = toHtml(h('', 'Just some words.'));
const actualHast = h('');
actualHast.children = wrapBareUrls(noBareUrls);
expect(toHtml(actualHast)).toEqual(expected);
});
it('should not trim whitespace', () => {
const noBareUrls = u('text', ' \n Just some words. ');
const expected = toHtml(h('', ' \n Just some words. '));
const actualHast = h('');
actualHast.children = wrapBareUrls(noBareUrls);
expect(toHtml(actualHast)).toEqual(expected);
});
it('should handle !', () => {
const exclamation = u('text', 'Just < : > near https://example.com!?');
const childrenExclamation = wrapBareUrls(exclamation);
const actualHast = h('');
actualHast.children = childrenExclamation;
const expected = toHtml(
h('', ['Just < : > near ', h('code', 'https://example.com'), '!?'])
);
expect(toHtml(actualHast)).toEqual(expected);
});
it('should not parse any markdown', () => {
const noBareUrls = u('text', 'Just *some* words.');
const childrenNoBare = wrapBareUrls(noBareUrls);
const actualHast = h('');
actualHast.children = childrenNoBare;
const expected = toHtml(h('', 'Just *some* words.'));
expect(toHtml(actualHast)).toEqual(expected);
});
it('should replace bare urls with code elements', () => {
const urlBare = u('text', 'a https://example.com b');
const childrenBare = wrapBareUrls(urlBare);
const actualHast = h('');
actualHast.children = childrenBare;
const expected = toHtml(
h('', ['a ', h('code', 'https://example.com'), ' b'])
);
expect(toHtml(actualHast)).toEqual(expected);
});
it('should replace url strings with code elements', () => {
const urlBare = u('text', 'https://example.com');
const childrenBare = wrapBareUrls(urlBare);
const actualHast = h('');
actualHast.children = childrenBare;
const expected = toHtml(h('', [h('code', 'https://example.com')]));
expect(toHtml(actualHast)).toEqual(expected);
});
it('should replace two bare urls with two code elements', () => {
const urlBare = u(
'text',
'a https://example.com text https://sample-site.com b'
);
const childrenBare = wrapBareUrls(urlBare);
const actualHast = h('');
actualHast.children = childrenBare;
const expected = toHtml(
h('', [
'a ',
h('code', 'https://example.com'),
' text ',
h('code', 'https://sample-site.com'),
' b'
])
);
expect(toHtml(actualHast)).toEqual(expected);
});
it('should replace two identical urls with two code elements', () => {
const urlBare = u(
'text',
'a https://example.com text https://example.com b'
);
const childrenBare = wrapBareUrls(urlBare);
const actualHast = h('');
actualHast.children = childrenBare;
const expected = toHtml(
h('', [
'a ',
h('code', 'https://example.com'),
' text ',
h('code', 'https://example.com'),
' b'
])
);
expect(toHtml(actualHast)).toEqual(expected);
});
it('should replace quoted bare urls with code elements', () => {
const urlQuoted = {
type: 'text',
value: 'a "https://example.com" b'
};
const childrenQuoted = wrapBareUrls(urlQuoted);
const actualQuoted = h('');
actualQuoted.children = childrenQuoted;
const expectedQuoted = toHtml(
h('', ['a "', h('code', 'https://example.com'), '" b'])
);
expect(toHtml(actualQuoted)).toEqual(expectedQuoted);
});
it('should replace single quoted bare urls with code elements', () => {
const urlQuoted = {
type: 'text',
value: `a 'https://example.com' b`
};
const childrenQuoted = wrapBareUrls(urlQuoted);
const actualQuoted = h('');
actualQuoted.children = childrenQuoted;
const expectedQuoted = toHtml(
h('', [`a '`, h('code', 'https://example.com'), `' b`])
);
expect(toHtml(actualQuoted)).toEqual(expectedQuoted);
});
// NOTE: this is a remark-parse bug that the formatter works around
it(`should replace quoted bare urls before '.' with code elements`, () => {
const urlQuoted = {
type: 'text',
value: '"http://example.com".'
};
const childrenQuoted = wrapBareUrls(urlQuoted);
const actualQuoted = h('');
actualQuoted.children = childrenQuoted;
const expectedQuoted = toHtml(
h('', ['"', h('code', 'http://example.com'), '".'])
);
expect(toHtml(actualQuoted)).toEqual(expectedQuoted);
});
// NOTE: this is a remark-parse bug that the formatter works around
it(`should replace single-quoted bare urls before '.' with code elements`, () => {
const urlQuoted = {
type: 'text',
value: "'http://example.com'."
};
const childrenQuoted = wrapBareUrls(urlQuoted);
const actualQuoted = h('');
actualQuoted.children = childrenQuoted;
const expectedQuoted = toHtml(
h('', ["'", h('code', 'http://example.com'), "'."])
);
expect(toHtml(actualQuoted)).toEqual(expectedQuoted);
});
// NOTE: this is a remark-parse bug that the formatter works around
it(`should replace quoted bare urls before '>' with code elements`, () => {
const urlQuoted = {
type: 'text',
value: '"http://example.com">this '
};
const childrenQuoted = wrapBareUrls(urlQuoted);
const actualQuoted = h('');
actualQuoted.children = childrenQuoted;
const expectedQuoted = toHtml(
h('', ['"', h('code', 'http://example.com'), '">this '])
);
expect(toHtml(actualQuoted)).toEqual(expectedQuoted);
});
// NOTE: this is a remark-parse bug that the formatter works around
it(`really should replace quoted bare urls before '>' with code elements`, () => {
const urlQuoted = {
type: 'text',
value: '"http://example.com">'
};
const childrenQuoted = wrapBareUrls(urlQuoted);
const actualQuoted = h('');
actualQuoted.children = childrenQuoted;
const expectedQuoted = toHtml(
h('', ['"', h('code', 'http://example.com'), '">'])
);
expect(toHtml(actualQuoted)).toEqual(expectedQuoted);
});
// NOTE: this is a remark-parse bug that the formatter works around
it(`should replace single-quoted bare urls before '>' with code elements`, () => {
const urlQuoted = {
type: 'text',
value: `'http://example.com'>this `
};
const childrenQuoted = wrapBareUrls(urlQuoted);
const actualQuoted = h('');
actualQuoted.children = childrenQuoted;
const expectedQuoted = toHtml(
h('', ["'", h('code', 'http://example.com'), "'>this "])
);
expect(toHtml(actualQuoted)).toEqual(expectedQuoted);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,31 @@
{
"name": "fcc-md-to-gfm",
"version": "1.0.0",
"description": "",
"main": "formatCurriculum.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "BSD-3-Clause",
"devDependencies": {
"hast-util-phrasing": "^1.0.5",
"hast-util-raw": "^6.0.1",
"hast-util-to-html": "^7.1.1",
"hast-util-to-mdast": "^6.0.0",
"hastscript": "^6.0.0",
"lodash": "^4.17.20",
"mdast-builder": "^1.1.1",
"mdast-util-to-hast": "^10.0.0",
"parse-entities": "^2.0.0",
"readdirp-walk": "^1.7.0",
"remark-frontmatter": "^2.0.0",
"remark-parse": "^8.0.3",
"remark-stringify": "^8.1.1",
"to-vfile": "^6.1.0",
"unified": "^9.2.0",
"unist-builder": "^2.0.3",
"unist-util-find": "^1.0.2",
"unist-util-visit": "^2.0.3"
}
}

View File

@ -0,0 +1,57 @@
const unified = require('unified');
const vfile = require('to-vfile');
const markdown = require('remark-parse');
const frontmatter = require('remark-frontmatter');
const stringify = require('remark-stringify');
const { insertSpaces } = require('./insert-spaces');
const codeToBackticks = require('./code-to-backticks');
const insertSpacesProcessor = unified()
.use(markdown)
.use(insertSpaces)
.use(stringify, { fences: true, emphasis: '*' })
.use(frontmatter, ['yaml']);
// ^ Prevents the frontmatter being modified
const getCodeToBackticksProcessor = (shouldThrow, pre) =>
unified()
.use(markdown)
.use(codeToBackticks, { shouldThrow, pre })
.use(stringify, { fences: true, emphasis: '*' })
.use(frontmatter, ['yaml']);
// Despite entities defaulting to false, some will still remain, including
// &lt;
const prettifyProcessor = unified()
.use(markdown)
.use(stringify, { fences: true, emphasis: '*' })
.use(frontmatter, ['yaml']);
exports.insertSpaces = createProcessor(insertSpacesProcessor);
exports.codeToBackticks = createProcessor(getCodeToBackticksProcessor());
exports.checkCodeToBackticks = createProcessor(
getCodeToBackticksProcessor(true)
);
exports.getCodeToBackticksSync = shouldThrow => text =>
getCodeToBackticksProcessor(shouldThrow, true).processSync(text);
exports.prettify = createProcessor(prettifyProcessor);
exports.prettifySync = prettifyProcessor.processSync;
function createProcessor(processor) {
return (msg, isFile = false) => {
const fileOrText = isFile ? vfile.readSync(msg) : msg;
return new Promise((resolve, reject) =>
processor.process(fileOrText, function(err, file) {
if (err) {
err.message += ' in file ' + msg;
reject(err);
}
return resolve(file.contents);
})
);
};
}

View File

@ -0,0 +1,30 @@
const { codeToBackticks } = require('./transformChallenges');
/* global expect */
describe('transformChallenges', () => {
describe('codeToBackticks', () => {
it('should convert <em> to *', () => {
const expected = 'Some *emphasis* here\n';
return codeToBackticks('Some <em>emphasis</em> here').then(actual => {
expect(actual).toEqual(expected);
});
});
it('should convert <code> to `', () => {
const expected = 'Code `code` test\n';
return codeToBackticks('Code <code>code</code> test').then(actual => {
expect(actual).toEqual(expected);
});
});
it('should convert html entities', () => {
const expected =
'a `<input type="text" placeholder="this is placeholder text">` test\n';
return codeToBackticks(
// eslint-disable-next-line max-len
'a <code>&#60;input type="text" placeholder="this is placeholder text"&#62;</code> test\n'
).then(actual => {
expect(actual).toEqual(expected);
});
});
});
});

View File

@ -0,0 +1,14 @@
const { insertSpaces, checkCodeToBackticks } = require('./transformChallenges');
const readDirP = require('readdirp-walk');
const challengeDir = '../../../../curriculum/challenges/english';
readDirP({ root: challengeDir, fileFilter: ['*.md'] }).on('data', file => {
if (file.stat.isFile()) {
insertSpaces(file.fullPath, true)
.then(checkCodeToBackticks)
.catch(() => {
console.log(file.path);
});
}
});

View File

@ -0,0 +1,440 @@
const { isEmpty, pick } = require('lodash');
const yaml = require('js-yaml');
const he = require('he');
const prettier = require('prettier');
const prettierOptions = prettier.resolveConfig.sync();
const {
getCodeToBackticksSync,
prettifySync
} = require('../../formatter/fcc-md-to-gfm/transformChallenges');
const { correctUrl } = require('../../formatter/fcc-md-to-gfm/insert-spaces');
const codeToBackticksSync = getCodeToBackticksSync(true);
const unified = require('unified');
const remarkParse = require('remark-parse');
const find = require('unist-util-find');
const remark2rehype = require('remark-rehype');
const html = require('rehype-stringify');
const raw = require('rehype-raw');
var parser = unified().use(remarkParse);
var mdToHTML = unified()
.use(remarkParse)
.use(remark2rehype, { allowDangerousHtml: true })
.use(raw)
.use(html, { allowDangerousCharacters: true, allowDangerousHtml: true })
.processSync;
function parseMd(text) {
return parser.parse(text);
}
// inspired by wrapRecursive, but takes in text and outputs text.
function wrapUrls(rawText) {
const mdNode = parseMd(rawText);
const link = find(mdNode, { type: 'link' });
if (link) {
const url = correctUrl(link.url);
const pos = rawText.indexOf(url);
const head = rawText.slice(0, pos);
const tail = rawText.slice(pos + url.length);
const newText = head + '`' + url + '`' + wrapUrls(tail);
return newText;
} else {
return rawText;
}
}
const frontmatterProperties = [
'id',
'title',
'challengeType',
'videoId',
'videoUrl',
'forumTopicId',
'isPrivate',
'required',
'helpCategory'
];
const otherProperties = [
'description',
'instructions',
'tests',
'solutions',
'files',
'question'
];
function createFrontmatter(data) {
Object.keys(data).forEach(key => {
if (!frontmatterProperties.includes(key) && !otherProperties.includes(key))
throw Error(`Unknown property '${key}'`);
});
// TODO: sort the keys? It doesn't matter from a machine perspective, but
// it does from human-readability one. We could get lucky and have the order
// be preserved accidentally.
const frontData = pick(data, frontmatterProperties);
const frontYAML = yaml.dump(frontData);
return `---
${frontYAML}---
`;
}
// TODO: handle certs elsewhere (ideally don't try to create mdx versions)
function createHints({ tests, title }) {
if (!tests) return '';
const strTests = tests
.map(
({ text, testString }) => `${hintToMd(text, title)}
${'```js'}
${
typeof testString === 'string'
? prettier
.format(testString, { ...prettierOptions, parser: 'babel' })
.trim()
: ''
}
${'```'}
`
)
.join('\n');
return `# --hints--
${strTests}
`;
}
function validateHints({ tests, question, title }) {
if (tests) {
tests.forEach(({ text }) => {
validateAndLog(text, title, false);
});
}
if (question && question.text) {
validateAndLog(question.text, title, false);
}
if (question && question.answers) {
question.answers.forEach(text => {
validateAndLog(text, title, false);
});
}
}
function validateAndLog(text, title, log = true) {
const { valid, parsed, parsedSimplified, finalHint } = validateText(text);
if (!valid) {
if (log) {
console.log('original'.padEnd(8, ' '), text);
console.log('parsed'.padEnd(8, ' '), parsed);
console.log('finalP'.padEnd(8, ' '), parsedSimplified);
console.log('finalT'.padEnd(8, ' '), finalHint);
}
throw Error(title);
}
}
function validateText(text) {
// hints can be empty; empty hints don't need validating.
if (!text) {
return { valid: true };
}
// the trailing \n will not affect the final html, so we can trim. At worst
// there will be <br> difference between the two.
text = text.trim();
let parsed = mdToHTML(text).contents;
// parsed text is expected to get wrapped in p tags, so we remove them.
// NOTE: this is a bit zealous, but allowing any p tags generates a ton of
// false positives.
if (parsed.match(/^<p>.*<\/p>$/s)) {
parsed = parsed.replace(/<p>/g, '').replace(/<\/p>/g, '');
} else if (parsed.match(/^<p>.*<\/p>\n<pre>/)) {
parsed = parsed.match(/^<p>(.*)<\/p>/s)[1];
text = text.match(/^(.*?)```/s)[1];
} else if (
parsed.match(/^<pre><code>/) ||
parsed.match(/^<pre><code\s*class/)
) {
// TODO: figure out how to handle the
/*
question: | text
```lang
sfasfd
```
*/
// type of question. Actually, since this is already md format, we can
// probably let these through. Just check that they match the <p>stuff</p>
// <pre> blah and we should be okay.
// throw Error(`Unexpected parse result ${parsed}`);
return { valid: true, parsed };
} else {
throw Error(`Unexpected parse result ${parsed}`);
}
if (text === parsed) {
return { valid: true, parsed };
}
// it's possible the hint contained ` not code tags, so we replace in both
// also trimming because we know whitespace is actually preserved by the mdx
// parser
let finalParsed = parsed.replace(/<code>/g, '`').replace(/<\/code>/g, '`');
let finalHint = text.replace(/<code>/g, '`').replace(/<\/code>/g, '`');
// I've verified that whitespace is preserved by formatting and that the mdx
// parser also preserves it when it should (i.e. inside ``). So, for
// simplicity, I'm collapsing it here.
finalParsed = finalParsed.replace(/\s+/g, '');
finalHint = finalHint.replace(/\s+/g, '');
// TODO: is this too lax? Just forcing them both to use the decoded
// characters.
finalParsed = he.decode(finalParsed);
finalHint = he.decode(finalHint);
return {
valid: finalHint === finalParsed,
parsed,
parsedSimplified: finalParsed,
finalHint
};
}
function hintToMd(hint, title) {
// we're only interested in `code` and want to avoid catching ```code```
const codeRE = /(?<!`)`[^`]+`(?!`)/g;
let wrappedCode = wrapCode(hint, title);
let wrappedUrls = wrapUrls(wrappedCode, title);
const pretty = wrappedUrls.split(codeRE).map(text => {
// prettify discards whitespace, which we generally want to keep.
if (text.match(/^\s*$/)) {
return text;
} else {
// bit of hack: we need to keep trailing newlines because they might be
// meaningful. prettifySync should respect them, but it's clearly being
// overzealous.
const leadingBreaks = text.match(/^\n*/)[0];
const rest = text.slice(leadingBreaks.length);
// prettify *also* adds a trailing \n which we generally don't want to
// keep
return leadingBreaks + prettifySync(rest).contents.slice(0, -1);
}
});
const code = [...wrappedUrls.matchAll(codeRE)].map(match => match[0]);
let newHint = [];
pretty.forEach((text, idx) => {
newHint.push(text);
if (typeof code[idx] !== 'undefined') {
newHint.push(code[idx]);
}
});
// depending on how the hint is represented in yaml, it can have extra \n
// chars at the end, so we should trim.
newHint = newHint.join('').trim();
return newHint;
}
function wrapCode(hint) {
if (typeof hint !== 'string') {
return '';
}
let mdHint;
function replacer(match, p1) {
// transform then remove the trailing \n
// Using pre is a dirty hack to make sure the whitespace is preserved
return codeToBackticksSync(`<pre>${p1}</pre>`).contents.slice(0, -1);
}
const codeRE = /<code>(.*?)<\/code>/g;
// to avoid parsing the rest of the markdown we use codeToBackticksSync on the
// code inside the re. If it fails, the code could be complicated enough to
// fool the regex, so we log it for human validation.
try {
mdHint = hint.replace(codeRE, replacer);
} catch (err) {
// console.log('err', err);
// console.log(`${title} failed
// hint:
// ${hint}`);
mdHint = hint.replace(codeRE, '`$1`');
// console.log('produced:');
// console.log(mdHint);
// console.log();
}
return mdHint;
}
function createSolutions({ solutions }) {
if (!solutions) return '';
const solutionsStr = solutions.map(soln => solutionToText(soln)).join(`
---
`);
return `# --solutions--
${solutionsStr}`;
}
function createQuestion({ question, title }) {
if (!question) return '';
const { text, answers, solution } = question;
return `# --question--
## --text--
${hintToMd(text, title)}
## --answers--
${answers.map(answer => hintToMd(answer, title)).join(`
---
`)}
## --video-solution--
${solution}
`;
}
// files: {
// indexhtml: {
// key: 'indexhtml',
// ext: 'html',
// name: 'index',
// contents: '<h1>Hello</h1>\n',
// head: '',
// tail: '',
// editableRegionBoundaries: []
// }
// }
function createSeed({ files }) {
if (!files) return '';
const supportedLanguages = ['html', 'css', 'js', 'jsx'];
const supportedIndexes = supportedLanguages.map(lang => 'index' + lang);
Object.values(files).forEach(({ ext }) => {
if (!supportedLanguages.includes(ext)) throw `Unsupported language: ${ext}`;
});
Object.keys(files).forEach(index => {
if (!supportedIndexes.includes(index)) throw `Unsupported index: ${index}`;
});
const head = Object.values(files)
.filter(({ head }) => !isEmpty(head))
.map(({ ext, head }) => fenceCode(ext, head))
.join('\n');
const tail = Object.values(files)
.filter(({ tail }) => !isEmpty(tail))
.map(({ ext, tail }) => fenceCode(ext, tail))
.join('\n');
const contents = Object.values(files)
.map(({ ext, contents, editableRegionBoundaries }) =>
fenceCode(ext, insertMarkers(contents, editableRegionBoundaries))
)
.join('\n');
return (
`# --seed--
` +
createSection('before-user-code', head, 2) +
createSection('after-user-code', tail, 2) +
createSection('seed-contents', contents, 2)
);
}
function insertMarkers(code, markers) {
const lines = code.split('\n');
return markers
.reduce((acc, idx) => {
return insert(acc, '--fcc-editable-region--', idx);
}, lines)
.join('\n');
}
function insert(xs, x, idx) {
return [...xs.slice(0, idx), x, ...xs.slice(idx)];
}
function solutionToText(solution) {
const supportedLanguages = ['html', 'css', 'js', 'jsx', 'py'];
const supportedIndexes = supportedLanguages.map(lang => 'index' + lang);
Object.values(solution).forEach(({ ext }) => {
if (!supportedLanguages.includes(ext)) throw `Unsupported language: ${ext}`;
});
Object.keys(solution).forEach(index => {
if (!supportedIndexes.includes(index)) throw `Unsupported index: ${index}`;
});
return Object.values(solution)
.map(({ ext, contents }) => fenceCode(ext, contents))
.join('\n');
}
// Even if there is no code, we should fence it in case the extension is used
function fenceCode(ext, code) {
return `${'```' + ext}
${code + '```'}
`;
}
function createInstructions({ instructions }) {
return createSection('instructions', instructions);
}
function createDescription({ description }) {
return createSection('description', description);
}
function createSection(heading, contents, depth = 1) {
return contents && contents.trim()
? `${''.padEnd(depth, '#')} --${heading}--
${contents}
`
: '';
}
function challengeToString(data) {
return (
createFrontmatter(data) +
createDescription(data) +
createInstructions(data) +
createQuestion(data) +
createHints(data) +
createSeed(data) +
createSolutions(data)
);
}
exports.challengeToString = challengeToString;
exports.validateHints = validateHints;
// console.log(exports.challengeToString(challengeData));
// // exports.challengeToString(challengeData);
// console.log(
// hintToMd('ZigZagMatrix(2) should return [[0, 1], [2, 3]].', 'title')
// );
// console.log(hintToMd('lar@freecodecamp.org', 'title'));

View File

@ -0,0 +1,28 @@
const readDirP = require('readdirp-walk');
const { getText } = require('./transform-to-mdx');
const { challengeToString } = require('./create-mdx');
const { parseMD } = require('../../../challenge-md-parser/mdx/index');
const fs = require('fs');
const challengeDir = '../../../../curriculum/challenges/chinese';
readDirP({ root: challengeDir, fileFilter: ['*.md'] }).on('data', file => {
if (file.stat.isFile()) {
generateTranscribableChallenge(file.fullPath)
.then(challengeToString)
.then(text => fs.writeFileSync(file.fullPath + 'x', text))
.catch(err => {
console.log('Error transforming');
console.log(file.path);
console.log('mdx version not created.');
console.log(err);
});
}
});
function generateTranscribableChallenge(fullPath) {
return Promise.all([parseMD(fullPath), getText(fullPath)]).then(results => ({
...results[0],
...results[1]
}));
}

View File

@ -0,0 +1,22 @@
const visit = require('unist-util-visit');
const is = require('unist-util-is');
function plugin() {
return transformer;
function transformer(tree) {
visit(tree, 'heading', visitor);
// eslint-disable-next-line consistent-return
function visitor(node) {
if (node.children.length !== 1) throw 'Heading has too many children';
if (is(node.children[0], { type: 'text', value: 'Description' }))
return true;
if (is(node.children[0], { type: 'text', value: 'Instructions' }))
return true;
}
}
}
module.exports = plugin;

View File

@ -0,0 +1,24 @@
const visit = require('unist-util-visit');
const YAML = require('js-yaml');
function plugin() {
return transformer;
function transformer(tree, file) {
visit(tree, 'code', visitor);
function visitor(node) {
const { lang, value } = node;
if (lang === 'yml') {
const tests = YAML.load(value);
file.data = {
...file.data,
...tests
};
}
}
}
}
module.exports = plugin;

View File

@ -0,0 +1,30 @@
const stringify = require('remark-stringify');
const { root } = require('mdast-builder');
const unified = require('unified');
const getAllBetween = require('../utils/get-all-between');
const stringifyMd = nodes =>
unified()
.use(stringify, { fences: true, emphasis: '*' })
.stringify(root(nodes));
function plugin() {
return transformer;
function transformer(tree, file) {
const descriptionNodes = getAllBetween(
tree,
{ type: 'html', value: "<section id='description'>" },
{ type: 'html', value: '</section>' }
);
file.data.description = stringifyMd(descriptionNodes);
const instructionsNodes = getAllBetween(
tree,
{ type: 'html', value: "<section id='instructions'>" },
{ type: 'html', value: '</section>' }
);
file.data.instructions = stringifyMd(instructionsNodes);
}
}
module.exports = plugin;

View File

@ -0,0 +1,24 @@
const unified = require('unified');
const vfile = require('to-vfile');
const markdown = require('remark-parse');
const frontmatter = require('remark-frontmatter');
const textToData = require('./plugins/text-to-data');
const testsToData = require('./plugins/tests-to-data');
const textProcessor = unified()
.use(markdown)
.use(textToData)
.use(testsToData)
.use(frontmatter, ['yaml']);
exports.getText = createProcessor(textProcessor);
function createProcessor(processor) {
return async msg => {
const file = typeof msg === 'string' ? vfile.readSync(msg) : msg;
const tree = processor.parse(file);
await processor.run(tree, file);
return file.data;
};
}

View File

@ -0,0 +1,19 @@
const between = require('unist-util-find-all-between');
const find = require('unist-util-find');
const findAfter = require('unist-util-find-after');
const findAllAfter = require('unist-util-find-all-after');
function getAllBetween(tree, testStart, testEnd) {
const start = find(tree, testStart);
if (!start) return [];
const end = findAfter(tree, start, testEnd);
const targetNodes = end
? between(tree, start, end)
: findAllAfter(tree, start);
return targetNodes;
}
module.exports = getAllBetween;

View File

@ -0,0 +1,19 @@
const readDirP = require('readdirp-walk');
const { getText } = require('./transform-to-mdx');
const { validateHints } = require('./create-mdx');
const challengeDir = '../../../../curriculum/challenges/english';
readDirP({
root: challengeDir,
fileFilter: ['*.md']
}).on('data', file => {
if (file.stat.isFile()) {
getText(file.fullPath)
.then(validateHints)
.catch(() => {
console.log('invalid hint in');
console.log(file.path);
});
}
});

2185
tools/scripts/formatter/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,41 @@
{
"name": "fcc-formatter",
"version": "1.0.0",
"description": "",
"main": "formatCurriculum.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "BSD-3-Clause",
"devDependencies": {
"hast-util-phrasing": "^1.0.5",
"hast-util-raw": "^6.0.1",
"hast-util-to-html": "^7.1.1",
"hast-util-to-mdast": "^6.0.0",
"hastscript": "^6.0.0",
"he": "^1.2.0",
"js-yaml": "^3.14.1",
"lodash": "^4.17.20",
"mdast-builder": "^1.1.1",
"mdast-util-to-hast": "^10.0.0",
"parse-entities": "^2.0.0",
"prettier": "^2.2.1",
"readdirp-walk": "^1.7.0",
"rehype-raw": "^5.0.0",
"rehype-stringify": "^8.0.0",
"remark-frontmatter": "^2.0.0",
"remark-parse": "^8.0.3",
"remark-rehype": "^8.0.0",
"remark-stringify": "^8.1.1",
"to-vfile": "^6.1.0",
"unified": "^9.2.0",
"unist-util-find": "^1.0.2",
"unist-util-find-after": "^3.0.0",
"unist-util-find-all-after": "^3.0.2",
"unist-util-find-all-between": "^2.1.0",
"unist-util-is": "^4.0.4",
"unist-util-visit": "^2.0.3"
},
"dependencies": {}
}