diff --git a/client/src/templates/Challenges/components/Challenge-Description.js b/client/src/templates/Challenges/components/Challenge-Description.js
index cfcadc8baf..ad6fcfd819 100644
--- a/client/src/templates/Challenges/components/Challenge-Description.js
+++ b/client/src/templates/Challenges/components/Challenge-Description.js
@@ -1,7 +1,7 @@
-import React, { Fragment, Component } from 'react';
-import Prism from 'prismjs';
+import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
+import PrismFormatted from './PrismFormatted';
import './challenge-description.css';
const propTypes = {
@@ -10,37 +10,19 @@ const propTypes = {
section: PropTypes.string
};
-class ChallengeDescription extends Component {
- componentDidMount() {
- // Just in case 'current' has not been created, though it should have been.
- if (this.instructionsRef.current) {
- Prism.highlightAllUnder(this.instructionsRef.current);
- }
- }
-
- constructor(props) {
- super(props);
- this.instructionsRef = React.createRef();
- }
-
- render() {
- const { description, instructions, section } = this.props;
- return (
-
-
- {instructions && (
-
-
-
-
- )}
-
-
- );
- }
+function ChallengeDescription({ description, instructions, section }) {
+ return (
+
+
+ {instructions && (
+
+
+
+
+ )}
+
+
+ );
}
ChallengeDescription.displayName = 'ChallengeDescription';
diff --git a/client/src/templates/Challenges/components/PrismFormatted.js b/client/src/templates/Challenges/components/PrismFormatted.js
new file mode 100644
index 0000000000..9c4aa4d077
--- /dev/null
+++ b/client/src/templates/Challenges/components/PrismFormatted.js
@@ -0,0 +1,38 @@
+import React, { Component } from 'react';
+import Prism from 'prismjs';
+import PropTypes from 'prop-types';
+
+const propTypes = {
+ className: PropTypes.string,
+ text: PropTypes.string.isRequired
+};
+
+class PrismFormatted extends Component {
+ componentDidMount() {
+ // Just in case 'current' has not been created, though it should have been.
+ if (this.instructionsRef.current) {
+ Prism.highlightAllUnder(this.instructionsRef.current);
+ }
+ }
+
+ constructor(props) {
+ super(props);
+ this.instructionsRef = React.createRef();
+ }
+
+ render() {
+ const { text, className } = this.props;
+ return (
+
+ );
+ }
+}
+
+PrismFormatted.displayName = 'PrismFormatted';
+PrismFormatted.propTypes = propTypes;
+
+export default PrismFormatted;
diff --git a/client/src/templates/Challenges/components/SanitizedSpan.js b/client/src/templates/Challenges/components/SanitizedSpan.js
deleted file mode 100644
index 5b7e3d1094..0000000000
--- a/client/src/templates/Challenges/components/SanitizedSpan.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import sanitizeHtml from 'sanitize-html';
-
-const propTypes = {
- text: PropTypes.string.isRequired
-};
-
-function SanitizedSpan({ text = '' }) {
- const sanitizedText = sanitizeHtml(text, {
- allowedTags: ['b', 'i', 'em', 'strong', 'code', 'wbr', 'br', 'pre']
- });
- return ;
-}
-
-SanitizedSpan.displayName = 'SanitizedSpan';
-SanitizedSpan.propTypes = propTypes;
-
-export default SanitizedSpan;
diff --git a/client/src/templates/Challenges/components/SanitizedSpan.test.js b/client/src/templates/Challenges/components/SanitizedSpan.test.js
deleted file mode 100644
index 6bc764173f..0000000000
--- a/client/src/templates/Challenges/components/SanitizedSpan.test.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/* global expect */
-
-import React from 'react';
-import { render } from '@testing-library/react';
-
-import SanitizedSpan from './SanitizedSpan';
-
-describe('', () => {
- it('matches the snapshot', () => {
- const { container } = render(
- dangerous code
- more text
`}
- />
- );
-
- expect(container).toMatchSnapshot();
- });
-
- it('removes scripts, images, etc', () => {
- const { queryByAltText, queryByText } = render(
- dangerous code
- more text
`}
- />
- );
-
- expect(queryByText('dangerous code', { ignore: false })).toBeNull();
- expect(queryByAltText('danger')).toBeNull();
- });
-
- it('leaves in line breaks', () => {
- const { container } = render(
-
- more text`}
- />
- );
- expect(container.querySelector('br')).not.toBeNull();
- });
-});
diff --git a/client/src/templates/Challenges/components/__snapshots__/SanitizedSpan.test.js.snap b/client/src/templates/Challenges/components/__snapshots__/SanitizedSpan.test.js.snap
deleted file mode 100644
index b1e3fbbde9..0000000000
--- a/client/src/templates/Challenges/components/__snapshots__/SanitizedSpan.test.js.snap
+++ /dev/null
@@ -1,14 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[` matches the snapshot 1`] = `
-
-
- some text
- more
-
- text
-
-
-
-
-`;
diff --git a/client/src/templates/Challenges/video/Show.js b/client/src/templates/Challenges/video/Show.js
index 0606416cd5..3e38ccef64 100644
--- a/client/src/templates/Challenges/video/Show.js
+++ b/client/src/templates/Challenges/video/Show.js
@@ -10,7 +10,7 @@ import YouTube from 'react-youtube';
import { createSelector } from 'reselect';
// Local Utilities
-import SanitizedSpan from '../components/SanitizedSpan';
+import PrismFormatted from '../components/PrismFormatted';
import { ChallengeNode } from '../../../redux/propTypes';
import LearnLayout from '../../../components/layouts/Learn';
import ChallengeTitle from '../components/Challenge-Title';
@@ -211,7 +211,7 @@ export class Project extends Component {
-
+
{answers.map((option, index) => (
@@ -229,7 +229,10 @@ export class Project extends Component {
) : null}
-
+
))}
diff --git a/client/src/templates/Challenges/video/show.css b/client/src/templates/Challenges/video/show.css
index eea7bda54e..ed0eda2eca 100644
--- a/client/src/templates/Challenges/video/show.css
+++ b/client/src/templates/Challenges/video/show.css
@@ -62,3 +62,7 @@
border-radius: 50%;
transform: translate(-50%, -50%);
}
+
+.video-quiz-option > p {
+ margin: 0;
+}
diff --git a/tools/challenge-md-parser/__snapshots__/tests-to-data.test.js.snap b/tools/challenge-md-parser/__snapshots__/tests-to-data.test.js.snap
index d35e91aa65..500209cb91 100644
--- a/tools/challenge-md-parser/__snapshots__/tests-to-data.test.js.snap
+++ b/tools/challenge-md-parser/__snapshots__/tests-to-data.test.js.snap
@@ -10,3 +10,19 @@ Object {
],
}
`;
+
+exports[`tests-to-data plugin should match the video snapshot 1`] = `
+Object {
+ "question": Object {
+ "answers": Array [
+ "inline code
",
+ "some italics
",
+ " code in
code tags
",
+ ],
+ "solution": 3,
+ "text": "Question line one
+ var x = 'y';
+
",
+ },
+}
+`;
diff --git a/tools/challenge-md-parser/fixtures/video-challenge-md-ast.json b/tools/challenge-md-parser/fixtures/video-challenge-md-ast.json
new file mode 100644
index 0000000000..9161b038d3
--- /dev/null
+++ b/tools/challenge-md-parser/fixtures/video-challenge-md-ast.json
@@ -0,0 +1,197 @@
+{
+ "type": "root",
+ "children": [
+ {
+ "type": "yaml",
+ "value": "id: 5e9a093a74c4063ca6f7c151\ntitle: Jupyter Notebooks Importing and Exporting Data\nchallengeType: 11\nvideoId: k1msxD3JIxE",
+ "position": {
+ "start": {
+ "line": 1,
+ "column": 1,
+ "offset": 0
+ },
+ "end": {
+ "line": 6,
+ "column": 4,
+ "offset": 129
+ },
+ "indent": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ]
+ }
+ },
+ {
+ "type": "heading",
+ "depth": 2,
+ "children": [
+ {
+ "type": "text",
+ "value": "Description",
+ "position": {
+ "start": {
+ "line": 8,
+ "column": 4,
+ "offset": 134
+ },
+ "end": {
+ "line": 8,
+ "column": 15,
+ "offset": 145
+ },
+ "indent": []
+ }
+ }
+ ],
+ "position": {
+ "start": {
+ "line": 8,
+ "column": 1,
+ "offset": 131
+ },
+ "end": {
+ "line": 8,
+ "column": 15,
+ "offset": 145
+ },
+ "indent": []
+ }
+ },
+ {
+ "type": "html",
+ "value": "",
+ "position": {
+ "start": {
+ "line": 9,
+ "column": 1,
+ "offset": 146
+ },
+ "end": {
+ "line": 10,
+ "column": 11,
+ "offset": 183
+ },
+ "indent": [
+ 1
+ ]
+ }
+ },
+ {
+ "type": "heading",
+ "depth": 2,
+ "children": [
+ {
+ "type": "text",
+ "value": "Tests",
+ "position": {
+ "start": {
+ "line": 12,
+ "column": 4,
+ "offset": 188
+ },
+ "end": {
+ "line": 12,
+ "column": 9,
+ "offset": 193
+ },
+ "indent": []
+ }
+ }
+ ],
+ "position": {
+ "start": {
+ "line": 12,
+ "column": 1,
+ "offset": 185
+ },
+ "end": {
+ "line": 12,
+ "column": 9,
+ "offset": 193
+ },
+ "indent": []
+ }
+ },
+ {
+ "type": "html",
+ "value": "",
+ "position": {
+ "start": {
+ "line": 13,
+ "column": 1,
+ "offset": 194
+ },
+ "end": {
+ "line": 13,
+ "column": 21,
+ "offset": 214
+ },
+ "indent": []
+ }
+ },
+ {
+ "type": "code",
+ "lang": "yml",
+ "value": "question:\n text: |\n Question line one\n ```js\n var x = 'y';\n ```\n\n answers:\n - inline `code`\n - some *italics*\n - code in
code tags\n solution: 3",
+ "position": {
+ "start": {
+ "line": 15,
+ "column": 1,
+ "offset": 216
+ },
+ "end": {
+ "line": 28,
+ "column": 4,
+ "offset": 411
+ },
+ "indent": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1
+ ]
+ }
+ },
+ {
+ "type": "html",
+ "value": "",
+ "position": {
+ "start": {
+ "line": 30,
+ "column": 1,
+ "offset": 413
+ },
+ "end": {
+ "line": 30,
+ "column": 11,
+ "offset": 423
+ },
+ "indent": []
+ }
+ }
+ ],
+ "position": {
+ "start": {
+ "line": 1,
+ "column": 1,
+ "offset": 0
+ },
+ "end": {
+ "line": 32,
+ "column": 1,
+ "offset": 425
+ }
+ }
+}
diff --git a/tools/challenge-md-parser/tests-to-data.js b/tools/challenge-md-parser/tests-to-data.js
index 97d69d4239..f550bd2499 100644
--- a/tools/challenge-md-parser/tests-to-data.js
+++ b/tools/challenge-md-parser/tests-to-data.js
@@ -1,5 +1,20 @@
const visit = require('unist-util-visit');
const YAML = require('js-yaml');
+const unified = require('unified');
+const markdown = require('remark-parse');
+const remark2rehype = require('remark-rehype');
+const html = require('rehype-stringify');
+const raw = require('rehype-raw');
+
+const processor = unified()
+ .use(markdown)
+ .use(remark2rehype, { allowDangerousHTML: true })
+ .use(raw)
+ .use(html);
+
+function mdToHTML(str) {
+ return processor.processSync(str).toString();
+}
function plugin() {
return transformer;
@@ -11,6 +26,12 @@ function plugin() {
const { lang, value } = node;
if (lang === 'yml') {
const tests = YAML.load(value);
+ if (tests.question) {
+ tests.question.answers = tests.question.answers.map(answer =>
+ mdToHTML(answer)
+ );
+ tests.question.text = mdToHTML(tests.question.text);
+ }
file.data = {
...file.data,
...tests
@@ -21,3 +42,4 @@ function plugin() {
}
module.exports = plugin;
+module.exports.mdToHTML = mdToHTML;
diff --git a/tools/challenge-md-parser/tests-to-data.test.js b/tools/challenge-md-parser/tests-to-data.test.js
index 8c5b0b6347..4bb16f8bb3 100644
--- a/tools/challenge-md-parser/tests-to-data.test.js
+++ b/tools/challenge-md-parser/tests-to-data.test.js
@@ -1,7 +1,23 @@
/* global describe it expect beforeEach */
const mockAST = require('./fixtures/challenge-md-ast.json');
+const mockVideoAST = require('./fixtures/video-challenge-md-ast.json');
const testsToData = require('./tests-to-data');
+const { mdToHTML } = testsToData;
+
+describe('mdToHTML', () => {
+ it('converts Markdown to HTML', () => {
+ // a line of text on its own is parsed as a paragraph, hence the p tags
+ expect(mdToHTML('*it*')).toBe('it
');
+ });
+
+ it('preserves code language', () => {
+ expect(mdToHTML('```js\n var x = "y";\n```')).toBe(
+ ' var x = "y";\n
'
+ );
+ });
+});
+
describe('tests-to-data plugin', () => {
const plugin = testsToData();
let file = { data: {} };
@@ -29,8 +45,40 @@ describe('tests-to-data plugin', () => {
expect(testObject).toHaveProperty('text');
});
+ it('should generate a question object from a video challenge AST', () => {
+ expect.assertions(4);
+ plugin(mockVideoAST, file);
+ const testObject = file.data.question;
+ expect(Object.keys(testObject).length).toBe(3);
+ expect(testObject).toHaveProperty('text');
+ expect(testObject).toHaveProperty('solution');
+ expect(testObject).toHaveProperty('answers');
+ });
+
+ it('should convert question and answer markdown into html', () => {
+ plugin(mockVideoAST, file);
+ const testObject = file.data.question;
+ expect(Object.keys(testObject).length).toBe(3);
+ expect(testObject.text).toBe(
+ 'Question line one
\n' +
+ ` var x = 'y';\n` +
+ '
'
+ );
+ expect(testObject.solution).toBe(3);
+ expect(testObject.answers[0]).toBe('inline code
');
+ expect(testObject.answers[1]).toBe('some italics
');
+ expect(testObject.answers[2]).toBe(
+ ' code in
code tags
'
+ );
+ });
+
it('should have an output to match the snapshot', () => {
plugin(mockAST, file);
expect(file.data).toMatchSnapshot();
});
+
+ it('should match the video snapshot', () => {
+ plugin(mockVideoAST, file);
+ expect(file.data).toMatchSnapshot();
+ });
});