mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-19 18:18:27 -05:00
fix(client,challenge-parser): display highlighted text as span for language challenges (#63802)
This commit is contained in:
@@ -469,6 +469,19 @@ form {
|
|||||||
input:focus {
|
input:focus {
|
||||||
border-color: var(--tertiary-color);
|
border-color: var(--tertiary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Text within backticks in en-US, es, and zh-CN challenge files
|
||||||
|
* is parsed into spans with this class.
|
||||||
|
*/
|
||||||
|
.highlighted-text {
|
||||||
|
padding: 1px 4px;
|
||||||
|
background-color: var(--tertiary-background);
|
||||||
|
color: var(--tertiary-color);
|
||||||
|
border: 1px solid var(--gray-45);
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
padding: 1px 4px;
|
padding: 1px 4px;
|
||||||
background-color: var(--tertiary-background);
|
background-color: var(--tertiary-background);
|
||||||
|
|||||||
36
tools/challenge-parser/parser/__fixtures__/with-en-us-mcq.md
Normal file
36
tools/challenge-parser/parser/__fixtures__/with-en-us-mcq.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
id: with-en-us-mcq
|
||||||
|
title: en-US MCQ
|
||||||
|
challengeType: 19
|
||||||
|
lang: en-US
|
||||||
|
---
|
||||||
|
|
||||||
|
# --instructions--
|
||||||
|
|
||||||
|
Instructions containing `some code`.
|
||||||
|
|
||||||
|
# --questions--
|
||||||
|
|
||||||
|
## --text--
|
||||||
|
|
||||||
|
Question text containing `highlighted text`.
|
||||||
|
|
||||||
|
## --answers--
|
||||||
|
|
||||||
|
`correct answer`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
`wrong answer`
|
||||||
|
|
||||||
|
### --feedback--
|
||||||
|
|
||||||
|
Feedback text containing `highlighted text`.
|
||||||
|
|
||||||
|
## --video-solution--
|
||||||
|
|
||||||
|
1
|
||||||
|
|
||||||
|
# --explanation--
|
||||||
|
|
||||||
|
Explanation text containing `highlighted text`.
|
||||||
36
tools/challenge-parser/parser/__fixtures__/with-es-mcq.md
Normal file
36
tools/challenge-parser/parser/__fixtures__/with-es-mcq.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
id: with-es-mcq
|
||||||
|
title: Spanish MCQ
|
||||||
|
challengeType: 19
|
||||||
|
lang: es
|
||||||
|
---
|
||||||
|
|
||||||
|
# --instructions--
|
||||||
|
|
||||||
|
Instructions containing `texto resaltado`.
|
||||||
|
|
||||||
|
# --questions--
|
||||||
|
|
||||||
|
## --text--
|
||||||
|
|
||||||
|
Question text containing `texto resaltado`.
|
||||||
|
|
||||||
|
## --answers--
|
||||||
|
|
||||||
|
`correct answer`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
`wrong answer`
|
||||||
|
|
||||||
|
### --feedback--
|
||||||
|
|
||||||
|
Feedback text containing `texto resaltado`.
|
||||||
|
|
||||||
|
## --video-solution--
|
||||||
|
|
||||||
|
1
|
||||||
|
|
||||||
|
# --explanation--
|
||||||
|
|
||||||
|
Explanation text containing `texto resaltado`.
|
||||||
@@ -103,36 +103,36 @@ describe('add-quizzes plugin', () => {
|
|||||||
|
|
||||||
// Quiz 1, Question 2
|
// Quiz 1, Question 2
|
||||||
expect(secondQuestion.text).toBe(
|
expect(secondQuestion.text).toBe(
|
||||||
'<p>Quiz 1, question 2 with <code>中文</code></p>'
|
'<p>Quiz 1, question 2 with <span class="highlighted-text">中文</span></p>'
|
||||||
);
|
);
|
||||||
expect(secondQuestion.distractors[0]).toBe(
|
expect(secondQuestion.distractors[0]).toBe(
|
||||||
'<p>Quiz 1, question 2, distractor 1 with <code>中文</code></p>'
|
'<p>Quiz 1, question 2, distractor 1 with <span class="highlighted-text">中文</span></p>'
|
||||||
);
|
);
|
||||||
expect(secondQuestion.distractors[1]).toBe(
|
expect(secondQuestion.distractors[1]).toBe(
|
||||||
'<p>Quiz 1, question 2, distractor 2 with <code>中文</code></p>'
|
'<p>Quiz 1, question 2, distractor 2 with <span class="highlighted-text">中文</span></p>'
|
||||||
);
|
);
|
||||||
expect(secondQuestion.distractors[2]).toBe(
|
expect(secondQuestion.distractors[2]).toBe(
|
||||||
'<p>Quiz 1, question 2, distractor 3 with <code>中文</code></p>'
|
'<p>Quiz 1, question 2, distractor 3 with <span class="highlighted-text">中文</span></p>'
|
||||||
);
|
);
|
||||||
expect(secondQuestion.answer).toBe(
|
expect(secondQuestion.answer).toBe(
|
||||||
'<p>Quiz 1, question 2, answer with <code>中文</code></p>'
|
'<p>Quiz 1, question 2, answer with <span class="highlighted-text">中文</span></p>'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Quiz 1, Question 3
|
// Quiz 1, Question 3
|
||||||
expect(thirdQuestion.text).toBe(
|
expect(thirdQuestion.text).toBe(
|
||||||
'<p>Quiz 1, question 3 with <code>zhōng wén</code></p>'
|
'<p>Quiz 1, question 3 with <span class="highlighted-text">zhōng wén</span></p>'
|
||||||
);
|
);
|
||||||
expect(thirdQuestion.distractors[0]).toBe(
|
expect(thirdQuestion.distractors[0]).toBe(
|
||||||
'<p>Quiz 1, question 3, distractor 1 with <code>zhōng wén</code></p>'
|
'<p>Quiz 1, question 3, distractor 1 with <span class="highlighted-text">zhōng wén</span></p>'
|
||||||
);
|
);
|
||||||
expect(thirdQuestion.distractors[1]).toBe(
|
expect(thirdQuestion.distractors[1]).toBe(
|
||||||
'<p>Quiz 1, question 3, distractor 2 with <code>zhōng wén</code></p>'
|
'<p>Quiz 1, question 3, distractor 2 with <span class="highlighted-text">zhōng wén</span></p>'
|
||||||
);
|
);
|
||||||
expect(thirdQuestion.distractors[2]).toBe(
|
expect(thirdQuestion.distractors[2]).toBe(
|
||||||
'<p>Quiz 1, question 3, distractor 3 with <code>zhōng wén</code></p>'
|
'<p>Quiz 1, question 3, distractor 3 with <span class="highlighted-text">zhōng wén</span></p>'
|
||||||
);
|
);
|
||||||
expect(thirdQuestion.answer).toBe(
|
expect(thirdQuestion.answer).toBe(
|
||||||
'<p>Quiz 1, question 3, answer with <code>zhōng wén</code></p>'
|
'<p>Quiz 1, question 3, answer with <span class="highlighted-text">zhōng wén</span></p>'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ describe('add-text', () => {
|
|||||||
mockAST,
|
mockAST,
|
||||||
withSubSectionAST,
|
withSubSectionAST,
|
||||||
withNestedInstructionsAST,
|
withNestedInstructionsAST,
|
||||||
withChineseAST;
|
withChineseAST,
|
||||||
|
withEnUsAST,
|
||||||
|
withEsAST;
|
||||||
const descriptionId = 'description';
|
const descriptionId = 'description';
|
||||||
const instructionsId = 'instructions';
|
const instructionsId = 'instructions';
|
||||||
const missingId = 'missing';
|
const missingId = 'missing';
|
||||||
@@ -21,6 +23,8 @@ describe('add-text', () => {
|
|||||||
'with-nested-instructions.md'
|
'with-nested-instructions.md'
|
||||||
);
|
);
|
||||||
withChineseAST = await parseFixture('with-chinese-mcq.md');
|
withChineseAST = await parseFixture('with-chinese-mcq.md');
|
||||||
|
withEnUsAST = await parseFixture('with-en-us-mcq.md');
|
||||||
|
withEsAST = await parseFixture('with-es-mcq.md');
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -162,6 +166,20 @@ describe('add-text', () => {
|
|||||||
expect(file.data).toMatchSnapshot();
|
expect(file.data).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should render inline code as code elements when lang is undefined', () => {
|
||||||
|
const plugin = addText(['instructions', 'explanation']);
|
||||||
|
|
||||||
|
const defaultFile = { data: {} };
|
||||||
|
plugin(withChineseAST, defaultFile);
|
||||||
|
|
||||||
|
expect(defaultFile.data.instructions).toBe(
|
||||||
|
'<section id="instructions">\n<p>Instructions containing <code>汉字 (hàn zì)</code>.</p>\n</section>'
|
||||||
|
);
|
||||||
|
expect(defaultFile.data.explanation).toBe(
|
||||||
|
'<section id="explanation">\n<p><code>我是 (wǒ shì) Web 开发者 (kāi fā zhě)。</code> – I am a web developer.</p>\n<p><code>你好 (nǐ hǎo),我是王华 (wǒ shì Wang Hua),请问你叫什么名字 (qǐng wèn nǐ jiào shén me míng zi)?</code> – Hello, I am Wang Hua, may I ask what your name is?</p>\n</section>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should render Chinese inline code as ruby when lang is zh-CN', () => {
|
it('should render Chinese inline code as ruby when lang is zh-CN', () => {
|
||||||
const plugin = addText(['instructions', 'explanation']);
|
const plugin = addText(['instructions', 'explanation']);
|
||||||
|
|
||||||
@@ -175,4 +193,32 @@ describe('add-text', () => {
|
|||||||
'<section id="explanation">\n<p><ruby>我是<rp>(</rp><rt>wǒ shì</rt><rp>)</rp></ruby> Web <ruby>开发者<rp>(</rp><rt>kāi fā zhě</rt><rp>)</rp></ruby>。 – I am a web developer.</p>\n<p><ruby>你好<rp>(</rp><rt>nǐ hǎo</rt><rp>)</rp></ruby>,<ruby>我是王华<rp>(</rp><rt>wǒ shì Wang Hua</rt><rp>)</rp></ruby>,<ruby>请问你叫什么名字<rp>(</rp><rt>qǐng wèn nǐ jiào shén me míng zi</rt><rp>)</rp></ruby>? – Hello, I am Wang Hua, may I ask what your name is?</p>\n</section>'
|
'<section id="explanation">\n<p><ruby>我是<rp>(</rp><rt>wǒ shì</rt><rp>)</rp></ruby> Web <ruby>开发者<rp>(</rp><rt>kāi fā zhě</rt><rp>)</rp></ruby>。 – I am a web developer.</p>\n<p><ruby>你好<rp>(</rp><rt>nǐ hǎo</rt><rp>)</rp></ruby>,<ruby>我是王华<rp>(</rp><rt>wǒ shì Wang Hua</rt><rp>)</rp></ruby>,<ruby>请问你叫什么名字<rp>(</rp><rt>qǐng wèn nǐ jiào shén me míng zi</rt><rp>)</rp></ruby>? – Hello, I am Wang Hua, may I ask what your name is?</p>\n</section>'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should render inline code as span elements when lang is en-US', () => {
|
||||||
|
const plugin = addText(['instructions', 'explanation']);
|
||||||
|
|
||||||
|
const enUsFile = { data: { lang: 'en-US' } };
|
||||||
|
plugin(withEnUsAST, enUsFile);
|
||||||
|
|
||||||
|
expect(enUsFile.data.instructions).toBe(
|
||||||
|
'<section id="instructions">\n<p>Instructions containing <span class="highlighted-text">some code</span>.</p>\n</section>'
|
||||||
|
);
|
||||||
|
expect(enUsFile.data.explanation).toBe(
|
||||||
|
'<section id="explanation">\n<p>Explanation text containing <span class="highlighted-text">highlighted text</span>.</p>\n</section>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render inline code as span elements when lang is es', () => {
|
||||||
|
const plugin = addText(['instructions', 'explanation']);
|
||||||
|
|
||||||
|
const esFile = { data: { lang: 'es' } };
|
||||||
|
plugin(withEsAST, esFile);
|
||||||
|
|
||||||
|
expect(esFile.data.instructions).toBe(
|
||||||
|
'<section id="instructions">\n<p>Instructions containing <span class="highlighted-text">texto resaltado</span>.</p>\n</section>'
|
||||||
|
);
|
||||||
|
expect(esFile.data.explanation).toBe(
|
||||||
|
'<section id="explanation">\n<p>Explanation text containing <span class="highlighted-text">texto resaltado</span>.</p>\n</section>'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ describe('add-video-question plugin', () => {
|
|||||||
multipleQuestionAST,
|
multipleQuestionAST,
|
||||||
videoOutOfOrderAST,
|
videoOutOfOrderAST,
|
||||||
videoWithAudioAST,
|
videoWithAudioAST,
|
||||||
|
chineseVideoAST,
|
||||||
|
enUsVideoAST,
|
||||||
|
esVideoAST,
|
||||||
videoWithSolutionAboveNumberOfAnswersAST,
|
videoWithSolutionAboveNumberOfAnswersAST,
|
||||||
videoWithFeedbackTwiceInARow,
|
videoWithFeedbackTwiceInARow,
|
||||||
videoWithCorrectAnswerWithFeedback,
|
videoWithCorrectAnswerWithFeedback;
|
||||||
chineseVideoAST;
|
|
||||||
const plugin = addVideoQuestion();
|
const plugin = addVideoQuestion();
|
||||||
let file = { data: {} };
|
let file = { data: {} };
|
||||||
|
|
||||||
@@ -35,6 +37,8 @@ describe('add-video-question plugin', () => {
|
|||||||
'with-video-question-correct-answer-with-feedback.md'
|
'with-video-question-correct-answer-with-feedback.md'
|
||||||
);
|
);
|
||||||
chineseVideoAST = await parseFixture('with-chinese-mcq.md');
|
chineseVideoAST = await parseFixture('with-chinese-mcq.md');
|
||||||
|
enUsVideoAST = await parseFixture('with-en-us-mcq.md');
|
||||||
|
esVideoAST = await parseFixture('with-es-mcq.md');
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -211,4 +215,54 @@ describe('add-video-question plugin', () => {
|
|||||||
'<ruby>问<rp>(</rp><rt>wèn</rt><rp>)</rp></ruby>'
|
'<ruby>问<rp>(</rp><rt>wèn</rt><rp>)</rp></ruby>'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should render inline code as spans in question text, answers, and feedback for en-US', async () => {
|
||||||
|
const enUsFile = { data: { lang: 'en-US' } };
|
||||||
|
|
||||||
|
plugin(enUsVideoAST, enUsFile);
|
||||||
|
|
||||||
|
const question = enUsFile.data.questions[0];
|
||||||
|
|
||||||
|
expect(question.text).toBe(
|
||||||
|
'<p>Question text containing <span class="highlighted-text">highlighted text</span>.</p>'
|
||||||
|
);
|
||||||
|
|
||||||
|
const answer1 = question.answers[0];
|
||||||
|
expect(answer1.answer).toBe(
|
||||||
|
'<p><span class="highlighted-text">correct answer</span></p>'
|
||||||
|
);
|
||||||
|
|
||||||
|
const answer2 = question.answers[1];
|
||||||
|
expect(answer2.answer).toBe(
|
||||||
|
'<p><span class="highlighted-text">wrong answer</span></p>'
|
||||||
|
);
|
||||||
|
expect(answer2.feedback).toBe(
|
||||||
|
'<p>Feedback text containing <span class="highlighted-text">highlighted text</span>.</p>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render inline code as spans in question text, answers, and feedback for es', async () => {
|
||||||
|
const esFile = { data: { lang: 'es' } };
|
||||||
|
|
||||||
|
plugin(esVideoAST, esFile);
|
||||||
|
|
||||||
|
const question = esFile.data.questions[0];
|
||||||
|
|
||||||
|
expect(question.text).toBe(
|
||||||
|
'<p>Question text containing <span class="highlighted-text">texto resaltado</span>.</p>'
|
||||||
|
);
|
||||||
|
|
||||||
|
const answer1 = question.answers[0];
|
||||||
|
expect(answer1.answer).toBe(
|
||||||
|
'<p><span class="highlighted-text">correct answer</span></p>'
|
||||||
|
);
|
||||||
|
|
||||||
|
const answer2 = question.answers[1];
|
||||||
|
expect(answer2.answer).toBe(
|
||||||
|
'<p><span class="highlighted-text">wrong answer</span></p>'
|
||||||
|
);
|
||||||
|
expect(answer2.feedback).toBe(
|
||||||
|
'<p>Feedback text containing <span class="highlighted-text">texto resaltado</span>.</p>'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -91,22 +91,48 @@ function chineseInlineCodeHandler(state, node) {
|
|||||||
// If static text, return code
|
// If static text, return code
|
||||||
return {
|
return {
|
||||||
type: 'element',
|
type: 'element',
|
||||||
// TODO: change this to span
|
tagName: 'span',
|
||||||
// https://github.com/freeCodeCamp/language-curricula/issues/22
|
properties: { className: 'highlighted-text' },
|
||||||
tagName: 'code',
|
|
||||||
properties: {},
|
|
||||||
children: [{ type: 'text', value: node.value }]
|
children: [{ type: 'text', value: node.value }]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const rubyOptions = {
|
/**
|
||||||
|
* Custom handler for inline code to render as span elements
|
||||||
|
* @param {object} state - The state object from mdast-util-to-hast
|
||||||
|
* @param {object} node - The inlineCode node
|
||||||
|
* @returns {object} Hast element node
|
||||||
|
*/
|
||||||
|
function spanInlineCodeHandler(state, node) {
|
||||||
|
return {
|
||||||
|
type: 'element',
|
||||||
|
tagName: 'span',
|
||||||
|
properties: { className: 'highlighted-text' },
|
||||||
|
children: [{ type: 'text', value: node.value }]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const spanOrRubyOptions = {
|
||||||
handlers: {
|
handlers: {
|
||||||
inlineCode: chineseInlineCodeHandler
|
inlineCode: chineseInlineCodeHandler
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const createMdastToHtml = lang =>
|
const spanOptions = {
|
||||||
lang == 'zh-CN' ? x => mdastToHTML(x, rubyOptions) : mdastToHTML;
|
handlers: {
|
||||||
|
inlineCode: spanInlineCodeHandler
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createMdastToHtml = lang => {
|
||||||
|
if (lang === 'zh-CN') {
|
||||||
|
return x => mdastToHTML(x, spanOrRubyOptions);
|
||||||
|
} else if (lang === 'en-US' || lang === 'es') {
|
||||||
|
return x => mdastToHTML(x, spanOptions);
|
||||||
|
} else {
|
||||||
|
return mdastToHTML;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
parseHanziPinyinPairs,
|
parseHanziPinyinPairs,
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ describe('createMdastToHtml', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fallback to code element if pattern does not match', () => {
|
it('should fallback to span element if pattern does not match', () => {
|
||||||
const toHtml = createMdastToHtml('zh-CN');
|
const toHtml = createMdastToHtml('zh-CN');
|
||||||
const nodes = [
|
const nodes = [
|
||||||
{
|
{
|
||||||
@@ -210,11 +210,49 @@ describe('createMdastToHtml', () => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
const actual = toHtml(nodes, { lang: 'zh-CN' });
|
const actual = toHtml(nodes);
|
||||||
expect(actual).toBe('<p><code>你好</code> and <code>nǐ hǎo</code></p>');
|
expect(actual).toBe(
|
||||||
|
'<p><span class="highlighted-text">你好</span> and <span class="highlighted-text">nǐ hǎo</span></p>'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render as regular code when lang is not zh-CN', () => {
|
it('should render inline code as span when lang is en-US', () => {
|
||||||
|
const toHtml = createMdastToHtml('en-US');
|
||||||
|
const nodes = [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
children: [
|
||||||
|
{ type: 'text', value: 'This is ' },
|
||||||
|
{ type: 'inlineCode', value: 'highlighted text' },
|
||||||
|
{ type: 'text', value: '.' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const actual = toHtml(nodes);
|
||||||
|
expect(actual).toBe(
|
||||||
|
'<p>This is <span class="highlighted-text">highlighted text</span>.</p>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render inline code as span when lang is es', () => {
|
||||||
|
const toHtml = createMdastToHtml('es');
|
||||||
|
const nodes = [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
children: [
|
||||||
|
{ type: 'text', value: 'Esto texto ' },
|
||||||
|
{ type: 'inlineCode', value: 'está resaltado' },
|
||||||
|
{ type: 'text', value: '.' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const actual = toHtml(nodes);
|
||||||
|
expect(actual).toBe(
|
||||||
|
'<p>Esto texto <span class="highlighted-text">está resaltado</span>.</p>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render as regular code when lang is not zh-CN, en-US, or es', () => {
|
||||||
const toHtml = createMdastToHtml('zh');
|
const toHtml = createMdastToHtml('zh');
|
||||||
const nodes = [
|
const nodes = [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user