fix(client,challenge-parser): display highlighted text as span for language challenges (#63802)

This commit is contained in:
Huyen Nguyen
2025-12-11 11:03:16 -08:00
committed by GitHub
parent 42777ed305
commit 0e2f81831e
8 changed files with 273 additions and 24 deletions

View File

@@ -469,6 +469,19 @@ form {
input:focus {
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 {
padding: 1px 4px;
background-color: var(--tertiary-background);

View 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`.

View 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`.

View File

@@ -103,36 +103,36 @@ describe('add-quizzes plugin', () => {
// Quiz 1, Question 2
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(
'<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(
'<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(
'<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(
'<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
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(
'<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(
'<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(
'<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(
'<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>'
);
});
});

View File

@@ -7,7 +7,9 @@ describe('add-text', () => {
mockAST,
withSubSectionAST,
withNestedInstructionsAST,
withChineseAST;
withChineseAST,
withEnUsAST,
withEsAST;
const descriptionId = 'description';
const instructionsId = 'instructions';
const missingId = 'missing';
@@ -21,6 +23,8 @@ describe('add-text', () => {
'with-nested-instructions.md'
);
withChineseAST = await parseFixture('with-chinese-mcq.md');
withEnUsAST = await parseFixture('with-en-us-mcq.md');
withEsAST = await parseFixture('with-es-mcq.md');
});
beforeEach(() => {
@@ -162,6 +166,20 @@ describe('add-text', () => {
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', () => {
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>'
);
});
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>'
);
});
});

View File

@@ -8,10 +8,12 @@ describe('add-video-question plugin', () => {
multipleQuestionAST,
videoOutOfOrderAST,
videoWithAudioAST,
chineseVideoAST,
enUsVideoAST,
esVideoAST,
videoWithSolutionAboveNumberOfAnswersAST,
videoWithFeedbackTwiceInARow,
videoWithCorrectAnswerWithFeedback,
chineseVideoAST;
videoWithCorrectAnswerWithFeedback;
const plugin = addVideoQuestion();
let file = { data: {} };
@@ -35,6 +37,8 @@ describe('add-video-question plugin', () => {
'with-video-question-correct-answer-with-feedback.md'
);
chineseVideoAST = await parseFixture('with-chinese-mcq.md');
enUsVideoAST = await parseFixture('with-en-us-mcq.md');
esVideoAST = await parseFixture('with-es-mcq.md');
});
beforeEach(() => {
@@ -211,4 +215,54 @@ describe('add-video-question plugin', () => {
'<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>'
);
});
});

View File

@@ -91,22 +91,48 @@ function chineseInlineCodeHandler(state, node) {
// If static text, return code
return {
type: 'element',
// TODO: change this to span
// https://github.com/freeCodeCamp/language-curricula/issues/22
tagName: 'code',
properties: {},
tagName: 'span',
properties: { className: 'highlighted-text' },
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: {
inlineCode: chineseInlineCodeHandler
}
};
const createMdastToHtml = lang =>
lang == 'zh-CN' ? x => mdastToHTML(x, rubyOptions) : mdastToHTML;
const spanOptions = {
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 = {
parseHanziPinyinPairs,

View File

@@ -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 nodes = [
{
@@ -210,11 +210,49 @@ describe('createMdastToHtml', () => {
]
}
];
const actual = toHtml(nodes, { lang: 'zh-CN' });
expect(actual).toBe('<p><code>你好</code> and <code>nǐ hǎo</code></p>');
const actual = toHtml(nodes);
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 nodes = [
{