feat(client,challenge-parser): render Chinese as ruby markup (#63424)

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Huyen Nguyen
2025-11-10 11:25:57 -08:00
committed by GitHub
parent 14dd3c6b27
commit 0c844ab007
13 changed files with 601 additions and 128 deletions

View File

@@ -89,6 +89,10 @@ hr {
border-top: 1px solid var(--quaternary-background);
}
rt {
font-size: 0.8rem;
}
#___gatsby {
height: 100%;
}

View File

@@ -0,0 +1,52 @@
---
id: test-with-chinese-mcq
title: Ruby Test
challengeType: 19
lang: zh-CN
---
# --instructions--
Instructions containing `汉字 (hàn zì)`.
# --questions--
## --text--
Question text containing `汉字 (hàn zì)`.
## --answers--
`你好 (nǐ hǎo)`
### --feedback--
Feedback text.
---
`请 (qǐng)`
### --feedback--
`请 (qǐng)` is not correct.
---
`请问 (qǐng wèn)`
---
`问 (wèn)`
### --feedback--
Feedback text.
## --video-solution--
3
# --explanation--
Wang Hua uses `请问 (qǐng wèn)` to politely start her question.

View File

@@ -0,0 +1,69 @@
# --quizzes--
## --quiz--
### --question--
#### --text--
Quiz 1, question 1 with `中文 (zhōng wén)`
#### --distractors--
Quiz 1, question 1, distractor 1 with `中文 (zhōng wén)`
---
Quiz 1, question 1, distractor 2 with `中文 (zhōng wén)`
---
Quiz 1, question 1, distractor 3 with `中文 (zhōng wén)`
#### --answer--
Quiz 1, question 1, answer with `中文 (zhōng wén)`
### --question--
#### --text--
Quiz 1, question 2 with `中文`
#### --distractors--
Quiz 1, question 2, distractor 1 with `中文`
---
Quiz 1, question 2, distractor 2 with `中文`
---
Quiz 1, question 2, distractor 3 with `中文`
#### --answer--
Quiz 1, question 2, answer with `中文`
### --question--
#### --text--
Quiz 1, question 3 with `zhōng wén`
#### --distractors--
Quiz 1, question 3, distractor 1 with `zhōng wén`
---
Quiz 1, question 3, distractor 2 with `zhōng wén`
---
Quiz 1, question 3, distractor 3 with `zhōng wén`
#### --answer--
Quiz 1, question 3, answer with `zhōng wén`

View File

@@ -1,23 +1,23 @@
const { getSection } = require('./utils/get-section');
const mdastToHtml = require('./utils/mdast-to-html');
const { createMdastToHtml } = require('./utils/i18n-stringify');
const { splitOnThematicBreak } = require('./utils/split-on-thematic-break');
function plugin() {
return transformer;
function transformer(tree, file) {
const toHtml = createMdastToHtml(file.data.lang);
const assignmentNodes = getSection(tree, '--assignment--');
const assignment = getAssignments(assignmentNodes).filter(a => a != '');
function getAssignments(assignmentNodes) {
const assignmentGroups = splitOnThematicBreak(assignmentNodes);
return assignmentGroups.map(assignment => toHtml(assignment));
}
file.data.assignments = assignment;
}
function getAssignments(assignmentNodes) {
const assignmentGroups = splitOnThematicBreak(assignmentNodes);
return assignmentGroups.map(assignment => mdastToHtml(assignment));
file.data.assignments = getAssignments(assignmentNodes).filter(
a => a != ''
);
}
}

View File

@@ -1,14 +1,39 @@
const { root } = require('mdast-builder');
const { getSection, getAllSections } = require('./utils/get-section');
const mdastToHtml = require('./utils/mdast-to-html');
const { createMdastToHtml } = require('./utils/i18n-stringify');
const { splitOnThematicBreak } = require('./utils/split-on-thematic-break');
function plugin() {
return transformer;
function transformer(tree, file) {
const toHtml = createMdastToHtml(file.data.lang);
const quizzesNodes = getSection(tree, `--quizzes--`);
function getDistractors(distractorsNodes) {
const distractorsGroups = splitOnThematicBreak(distractorsNodes);
if (distractorsGroups.length !== 3)
throw Error('Three distractors are required per quiz-question');
return distractorsGroups.map(distractorsGroup => {
return toHtml(distractorsGroup);
});
}
function getQuestion(textNodes, distractorNodes, answerNodes) {
const text = toHtml(textNodes);
const distractors = getDistractors(distractorNodes);
const answer = toHtml(answerNodes);
if (!text) throw Error('--text-- is missing from the quiz question');
if (!distractors)
throw Error('--distractors-- are missing from quiz question');
if (!answer) throw Error('--answer-- is missing from quiz question');
return { text, distractors, answer };
}
if (quizzesNodes.length > 0) {
const compiledQuizzes = [];
const quizSections = getAllSections(root(quizzesNodes), `--quiz--`);
@@ -51,28 +76,4 @@ function plugin() {
}
}
function getQuestion(textNodes, distractorNodes, answerNodes) {
const text = mdastToHtml(textNodes);
const distractors = getDistractors(distractorNodes);
const answer = mdastToHtml(answerNodes);
if (!text) throw Error('--text-- is missing from the quiz question');
if (!distractors)
throw Error('--distractors-- are missing from quiz question');
if (!answer) throw Error('--answer-- is missing from quiz question');
return { text, distractors, answer };
}
function getDistractors(distractorsNodes) {
const distractorsGroups = splitOnThematicBreak(distractorsNodes);
if (distractorsGroups.length !== 3)
throw Error('Three distractors are required per quiz-question');
return distractorsGroups.map(distractorsGroup => {
return mdastToHtml(distractorsGroup);
});
}
module.exports = plugin;

View File

@@ -4,11 +4,13 @@ import addQuizzes from './add-quizzes';
describe('add-quizzes plugin', () => {
let mockQuizzesAST;
let chineseQuizzesAST;
const plugin = addQuizzes();
let file = { data: {} };
beforeAll(async () => {
mockQuizzesAST = await parseFixture('with-quizzes.md');
chineseQuizzesAST = await parseFixture('with-chinese-quizzes.md');
});
beforeEach(() => {
@@ -61,4 +63,76 @@ describe('add-quizzes plugin', () => {
plugin(mockQuizzesAST, file);
expect(file.data).toMatchSnapshot();
});
it('should render Chinese hanzi (pinyin) in quizzes when lang is zh-CN and the text contains hanzi (pinyin) format', () => {
const zhFile = { data: { lang: 'zh-CN' } };
plugin(chineseQuizzesAST, zhFile);
const quizzes = zhFile.data.quizzes;
expect(Array.isArray(quizzes)).toBe(true);
expect(quizzes.length).toBe(1);
const firstQuiz = quizzes[0];
const firstQuestion = firstQuiz.questions[0];
// Quiz 1, Question 1
expect(firstQuestion.text).toBe(
'<p>Quiz 1, question 1 with <ruby>中文<rp>(</rp><rt>zhōng wén</rt><rp>)</rp></ruby></p>'
);
expect(firstQuestion.distractors[0]).toBe(
'<p>Quiz 1, question 1, distractor 1 with <ruby>中文<rp>(</rp><rt>zhōng wén</rt><rp>)</rp></ruby></p>'
);
expect(firstQuestion.distractors[1]).toBe(
'<p>Quiz 1, question 1, distractor 2 with <ruby>中文<rp>(</rp><rt>zhōng wén</rt><rp>)</rp></ruby></p>'
);
expect(firstQuestion.distractors[2]).toBe(
'<p>Quiz 1, question 1, distractor 3 with <ruby>中文<rp>(</rp><rt>zhōng wén</rt><rp>)</rp></ruby></p>'
);
expect(firstQuestion.answer).toBe(
'<p>Quiz 1, question 1, answer with <ruby>中文<rp>(</rp><rt>zhōng wén</rt><rp>)</rp></ruby></p>'
);
});
it('should render Chinese in quizzes when lang is zh-CN and the text does not contain hanzi (pinyin) format', () => {
const zhFile = { data: { lang: 'zh-CN' } };
plugin(chineseQuizzesAST, zhFile);
const quizzes = zhFile.data.quizzes;
const firstQuiz = quizzes[0];
const secondQuestion = firstQuiz.questions[1];
const thirdQuestion = firstQuiz.questions[2];
// Quiz 1, Question 2
expect(secondQuestion.text).toBe(
'<p>Quiz 1, question 2 with <code>中文</code></p>'
);
expect(secondQuestion.distractors[0]).toBe(
'<p>Quiz 1, question 2, distractor 1 with <code>中文</code></p>'
);
expect(secondQuestion.distractors[1]).toBe(
'<p>Quiz 1, question 2, distractor 2 with <code>中文</code></p>'
);
expect(secondQuestion.distractors[2]).toBe(
'<p>Quiz 1, question 2, distractor 3 with <code>中文</code></p>'
);
expect(secondQuestion.answer).toBe(
'<p>Quiz 1, question 2, answer with <code>中文</code></p>'
);
// Quiz 1, Question 3
expect(thirdQuestion.text).toBe(
'<p>Quiz 1, question 3 with <code>zhōng wén</code></p>'
);
expect(thirdQuestion.distractors[0]).toBe(
'<p>Quiz 1, question 3, distractor 1 with <code>zhōng wén</code></p>'
);
expect(thirdQuestion.distractors[1]).toBe(
'<p>Quiz 1, question 3, distractor 2 with <code>zhōng wén</code></p>'
);
expect(thirdQuestion.distractors[2]).toBe(
'<p>Quiz 1, question 3, distractor 3 with <code>zhōng wén</code></p>'
);
expect(thirdQuestion.answer).toBe(
'<p>Quiz 1, question 3, answer with <code>zhōng wén</code></p>'
);
});
});

View File

@@ -2,7 +2,7 @@ const { isEmpty } = require('lodash');
const find = require('unist-util-find');
const { root } = require('mdast-builder');
const { getSection, isMarker } = require('./utils/get-section');
const mdastToHTML = require('./utils/mdast-to-html');
const { createMdastToHtml } = require('./utils/i18n-stringify');
function addText(sectionIds) {
if (!sectionIds || !Array.isArray(sectionIds) || sectionIds.length <= 0) {
@@ -17,7 +17,9 @@ function addText(sectionIds) {
`The --${sectionId}-- section should not have any subsections. Found subsection ${subSection.children[0].value}`
);
}
const sectionText = mdastToHTML(textNodes);
const toHtml = createMdastToHtml(file.data.lang);
const sectionText = toHtml(textNodes);
if (!isEmpty(sectionText)) {
file.data = {
...file.data,

View File

@@ -3,7 +3,11 @@ import parseFixture from '../__fixtures__/parse-fixture';
import addText from './add-text';
describe('add-text', () => {
let realisticAST, mockAST, withSubSectionAST, withNestedInstructionsAST;
let realisticAST,
mockAST,
withSubSectionAST,
withNestedInstructionsAST,
withChineseAST;
const descriptionId = 'description';
const instructionsId = 'instructions';
const missingId = 'missing';
@@ -16,6 +20,7 @@ describe('add-text', () => {
withNestedInstructionsAST = await parseFixture(
'with-nested-instructions.md'
);
withChineseAST = await parseFixture('with-chinese-mcq.md');
});
beforeEach(() => {
@@ -156,4 +161,18 @@ describe('add-text', () => {
plugin(mockAST, file);
expect(file.data).toMatchSnapshot();
});
it('should render Chinese inline code as ruby when lang is zh-CN', () => {
const plugin = addText(['instructions', 'explanation']);
const zhFile = { data: { lang: 'zh-CN' } };
plugin(withChineseAST, zhFile);
expect(zhFile.data.instructions).toBe(
'<section id="instructions">\n<p>Instructions containing <ruby>汉字<rp>(</rp><rt>hàn zì</rt><rp>)</rp></ruby>.</p>\n</section>'
);
expect(zhFile.data.explanation).toBe(
'<section id="explanation">\n<p>Wang Hua uses <ruby>请问<rp>(</rp><rt>qǐng wèn</rt><rp>)</rp></ruby> to politely start her question.</p>\n</section>'
);
});
});

View File

@@ -2,14 +2,103 @@ const { root } = require('mdast-builder');
const find = require('unist-util-find');
const { getSection } = require('./utils/get-section');
const getAllBefore = require('./utils/before-heading');
const mdastToHtml = require('./utils/mdast-to-html');
const { getParagraphContent } = require('./utils/get-paragraph-content');
const { splitOnThematicBreak } = require('./utils/split-on-thematic-break');
const { createMdastToHtml } = require('./utils/i18n-stringify');
function plugin() {
return transformer;
function transformer(tree, file) {
const toHtml = createMdastToHtml(file.data.lang);
function getQuestion(textNodes, answersNodes, solutionNodes) {
const text = toHtml(textNodes);
const answers = getAnswers(answersNodes);
const solution = getSolution(solutionNodes);
if (!text) throw Error('text is missing from question');
if (!answers) throw Error('answers are missing from question');
if (!solution) throw Error('solution is missing from question');
return { text, answers, solution };
}
function getAnswers(answersNodes) {
const answerGroups = splitOnThematicBreak(answersNodes);
return answerGroups.map(answerGroup => {
const answerTree = root(answerGroup);
const feedbackNodes = getSection(answerTree, '--feedback--');
const audioIdNodes = getSection(answerTree, '--audio-id--');
const hasFeedback = feedbackNodes.length > 0;
const hasAudioId = audioIdNodes.length > 0;
if (hasFeedback || hasAudioId) {
let answerNodes;
if (hasFeedback && hasAudioId) {
const feedbackHeading = find(answerTree, {
type: 'heading',
children: [{ type: 'text', value: '--feedback--' }]
});
const audioIdHeading = find(answerTree, {
type: 'heading',
children: [{ type: 'text', value: '--audio-id--' }]
});
const feedbackIndex = answerTree.children.indexOf(feedbackHeading);
const audioIdIndex = answerTree.children.indexOf(audioIdHeading);
const firstMarker =
feedbackIndex < audioIdIndex ? '--feedback--' : '--audio-id--';
answerNodes = getAllBefore(answerTree, firstMarker);
} else if (hasFeedback) {
answerNodes = getAllBefore(answerTree, '--feedback--');
} else {
answerNodes = getAllBefore(answerTree, '--audio-id--');
}
if (answerNodes.length < 1) {
throw Error('Answer missing');
}
let extractedAudioId = null;
if (hasAudioId) {
const audioIdContent = getParagraphContent(audioIdNodes[0]);
if (audioIdContent && audioIdContent.trim()) {
extractedAudioId = audioIdContent.trim();
}
}
return {
answer: toHtml(answerNodes),
feedback: hasFeedback ? toHtml(feedbackNodes) : null,
audioId: extractedAudioId
};
}
return {
answer: toHtml(answerGroup),
feedback: null,
audioId: null
};
});
}
function getSolution(solutionNodes) {
let solution;
try {
solution = Number(getParagraphContent(solutionNodes[0]));
if (Number.isNaN(solution)) throw Error('Not a number');
if (solution < 1) throw Error('Not positive number');
} catch (e) {
console.log(e);
throw Error('A video solution should be a positive integer');
}
return solution;
}
const allQuestionNodes = getSection(tree, '--questions--');
if (allQuestionNodes.length > 0) {
@@ -41,88 +130,4 @@ function plugin() {
}
}
function getQuestion(textNodes, answersNodes, solutionNodes) {
const text = mdastToHtml(textNodes);
const answers = getAnswers(answersNodes);
const solution = getSolution(solutionNodes);
if (!text) throw Error('text is missing from question');
if (!answers) throw Error('answers are missing from question');
if (!solution) throw Error('solution is missing from question');
return { text, answers, solution };
}
function getAnswers(answersNodes) {
const answerGroups = splitOnThematicBreak(answersNodes);
return answerGroups.map(answerGroup => {
const answerTree = root(answerGroup);
const feedbackNodes = getSection(answerTree, '--feedback--');
const audioIdNodes = getSection(answerTree, '--audio-id--');
const hasFeedback = feedbackNodes.length > 0;
const hasAudioId = audioIdNodes.length > 0;
if (hasFeedback || hasAudioId) {
let answerNodes;
if (hasFeedback && hasAudioId) {
const feedbackHeading = find(answerTree, {
type: 'heading',
children: [{ type: 'text', value: '--feedback--' }]
});
const audioIdHeading = find(answerTree, {
type: 'heading',
children: [{ type: 'text', value: '--audio-id--' }]
});
const feedbackIndex = answerTree.children.indexOf(feedbackHeading);
const audioIdIndex = answerTree.children.indexOf(audioIdHeading);
const firstMarker =
feedbackIndex < audioIdIndex ? '--feedback--' : '--audio-id--';
answerNodes = getAllBefore(answerTree, firstMarker);
} else if (hasFeedback) {
answerNodes = getAllBefore(answerTree, '--feedback--');
} else {
answerNodes = getAllBefore(answerTree, '--audio-id--');
}
if (answerNodes.length < 1) {
throw Error('Answer missing');
}
let extractedAudioId = null;
if (hasAudioId) {
const audioIdContent = getParagraphContent(audioIdNodes[0]);
if (audioIdContent && audioIdContent.trim()) {
extractedAudioId = audioIdContent.trim();
}
}
return {
answer: mdastToHtml(answerNodes),
feedback: hasFeedback ? mdastToHtml(feedbackNodes) : null,
audioId: extractedAudioId
};
}
return { answer: mdastToHtml(answerGroup), feedback: null, audioId: null };
});
}
function getSolution(solutionNodes) {
let solution;
try {
solution = Number(getParagraphContent(solutionNodes[0]));
if (Number.isNaN(solution)) throw Error('Not a number');
if (solution < 1) throw Error('Not positive number');
} catch (e) {
console.log(e);
throw Error('A video solution should be a positive integer');
}
return solution;
}
module.exports = plugin;

View File

@@ -7,7 +7,8 @@ describe('add-video-question plugin', () => {
videoAST,
multipleQuestionAST,
videoOutOfOrderAST,
videoWithAudioAST;
videoWithAudioAST,
chineseVideoAST;
const plugin = addVideoQuestion();
let file = { data: {} };
@@ -21,6 +22,7 @@ describe('add-video-question plugin', () => {
'with-video-question-out-of-order.md'
);
videoWithAudioAST = await parseFixture('with-video-question-audio.md');
chineseVideoAST = await parseFixture('with-chinese-mcq.md');
});
beforeEach(() => {
@@ -107,7 +109,7 @@ describe('add-video-question plugin', () => {
it('should NOT throw if there is no question', () => {
expect.assertions(1);
expect(() => plugin(simpleAST)).not.toThrow();
expect(() => plugin(simpleAST, file)).not.toThrow();
});
it('should extract audioId from answers when present', () => {
@@ -139,4 +141,39 @@ describe('add-video-question plugin', () => {
plugin(videoAST, file);
expect(file.data).toMatchSnapshot();
});
it('should render Chinese inline code as ruby in question text, answers, and feedback', async () => {
const zhFile = { data: { lang: 'zh-CN' } };
plugin(chineseVideoAST, zhFile);
const question = zhFile.data.questions[0];
expect(question.text).toBe(
'<p>Question text containing <ruby>汉字<rp>(</rp><rt>hàn zì</rt><rp>)</rp></ruby>.</p>'
);
const answer1 = question.answers[0];
expect(answer1.answer).toContain(
'<ruby>你好<rp>(</rp><rt>nǐ hǎo</rt><rp>)</rp></ruby>'
);
const answer2 = question.answers[1];
expect(answer2.answer).toContain(
'<ruby>请<rp>(</rp><rt>qǐng</rt><rp>)</rp></ruby>'
);
expect(answer2.feedback).toBe(
'<p><ruby>请<rp>(</rp><rt>qǐng</rt><rp>)</rp></ruby> is not correct.</p>'
);
const answer3 = question.answers[2];
expect(answer3.answer).toContain(
'<ruby>请问<rp>(</rp><rt>qǐng wèn</rt><rp>)</rp></ruby>'
);
const answer4 = question.answers[3];
expect(answer4.answer).toContain(
'<ruby>问<rp>(</rp><rt>wèn</rt><rp>)</rp></ruby>'
);
});
});

View File

@@ -0,0 +1,78 @@
const mdastToHTML = require('./mdast-to-html');
/**
* Parses Chinese text in format: hanzi (pinyin)
* @param {string} text - Text in format: hanzi (pinyin)
* @returns {{ hanzi: string, pinyin: string } | null} Parsed hanzi and pinyin, or null if not matching
*/
function parseChinesePattern(text) {
const match = text.match(/^(.+?)\s*\((.+?)\)$/);
if (!match) {
return null;
}
return {
hanzi: match[1].trim(),
pinyin: match[2].trim()
};
}
/**
* Custom handler for Chinese inline code to render as ruby elements
* @param {object} state - The state object from mdast-util-to-hast
* @param {object} node - The inlineCode node
* @returns {object} Hast element node
*/
function chineseInlineCodeHandler(state, node) {
const parsed = parseChinesePattern(node.value);
if (parsed) {
return {
type: 'element',
tagName: 'ruby',
properties: {},
children: [
{ type: 'text', value: parsed.hanzi },
{
type: 'element',
tagName: 'rp',
properties: {},
children: [{ type: 'text', value: '(' }]
},
{
type: 'element',
tagName: 'rt',
properties: {},
children: [{ type: 'text', value: parsed.pinyin }]
},
{
type: 'element',
tagName: 'rp',
properties: {},
children: [{ type: 'text', value: ')' }]
}
]
};
}
return {
type: 'element',
// TODO: change this to span
// https://github.com/freeCodeCamp/language-curricula/issues/22
tagName: 'code',
properties: {},
children: [{ type: 'text', value: node.value }]
};
}
const rubyOptions = {
handlers: {
inlineCode: chineseInlineCodeHandler
}
};
const createMdastToHtml = lang =>
lang == 'zh-CN' ? x => mdastToHTML(x, rubyOptions) : mdastToHTML;
module.exports = { parseChinesePattern, createMdastToHtml };

View File

@@ -0,0 +1,129 @@
import { describe, it, expect } from 'vitest';
import { createMdastToHtml, parseChinesePattern } from './i18n-stringify';
describe('parseChinesePattern', () => {
it('should parse Chinese text with hanzi and pinyin', () => {
const result = parseChinesePattern('你好 (nǐ hǎo)');
expect(result).toEqual({
hanzi: '你好',
pinyin: 'nǐ hǎo'
});
});
it('should handle text without spaces before parentheses', () => {
const result = parseChinesePattern('你好(nǐ hǎo)');
expect(result).toEqual({
hanzi: '你好',
pinyin: 'nǐ hǎo'
});
});
it('should handle text with multiple spaces', () => {
const result = parseChinesePattern('你好 (nǐ hǎo)');
expect(result).toEqual({
hanzi: '你好',
pinyin: 'nǐ hǎo'
});
});
it('should return null for text without parentheses', () => {
const result = parseChinesePattern('你好');
expect(result).toBeNull();
});
it('should return null for text with only opening parenthesis', () => {
const result = parseChinesePattern('你好 (nǐ hǎo');
expect(result).toBeNull();
});
it('should return null for empty string', () => {
const result = parseChinesePattern('');
expect(result).toBeNull();
});
});
describe('createMdastToHtml', () => {
it('should render Chinese inline code as ruby when lang is zh-CN', () => {
const toHtml = createMdastToHtml('zh-CN');
const nodes = [
{
type: 'paragraph',
children: [
{ type: 'text', value: 'This is ' },
{ type: 'inlineCode', value: '请问 (qǐng wèn)' },
{ type: 'text', value: '.' }
]
}
];
const actual = toHtml(nodes);
expect(actual).toBe(
'<p>This is <ruby>请问<rp>(</rp><rt>qǐng wèn</rt><rp>)</rp></ruby>.</p>'
);
});
it('should render Chinese inline code as ruby with or without space before parenthesis', () => {
const toHtml = createMdastToHtml('zh-CN');
const nodesWithSpace = [
{
type: 'paragraph',
children: [{ type: 'inlineCode', value: '你好 (nǐ hǎo)' }]
}
];
const nodesWithoutSpace = [
{
type: 'paragraph',
children: [{ type: 'inlineCode', value: '你好(nǐ hǎo)' }]
}
];
const expected =
'<p><ruby>你好<rp>(</rp><rt>nǐ hǎo</rt><rp>)</rp></ruby></p>';
expect(toHtml(nodesWithSpace)).toBe(expected);
expect(toHtml(nodesWithoutSpace)).toBe(expected);
});
it('should handle multiple Chinese inline codes in one paragraph', () => {
const toHtml = createMdastToHtml('zh-CN');
const nodes = [
{
type: 'paragraph',
children: [
{ type: 'inlineCode', value: '你好 (nǐ hǎo)' },
{ type: 'text', value: ' and ' },
{ type: 'inlineCode', value: '再见 (zài jiàn)' }
]
}
];
const actual = toHtml(nodes);
expect(actual).toBe(
'<p><ruby>你好<rp>(</rp><rt>nǐ hǎo</rt><rp>)</rp></ruby> and <ruby>再见<rp>(</rp><rt>zài jiàn</rt><rp>)</rp></ruby></p>'
);
});
it('should fallback to code element if pattern does not match', () => {
const toHtml = createMdastToHtml('zh-CN');
const nodes = [
{
type: 'paragraph',
children: [
{ type: 'inlineCode', value: '你好' },
{ type: 'text', value: ' and ' },
{ type: 'inlineCode', value: 'nǐ hǎo' }
]
}
];
const actual = toHtml(nodes, { lang: 'zh-CN' });
expect(actual).toBe('<p><code>你好</code> and <code>nǐ hǎo</code></p>');
});
it('should render as regular code when lang is not zh-CN', () => {
const toHtml = createMdastToHtml('zh');
const nodes = [
{
type: 'paragraph',
children: [{ type: 'inlineCode', value: '请问 (qǐng wèn)' }]
}
];
const actual = toHtml(nodes);
expect(actual).toBe('<p><code>请问 (qǐng wèn)</code></p>');
});
});

View File

@@ -2,14 +2,17 @@ const hastToHTML = require('hast-util-to-html');
const { root } = require('mdast-builder');
const mdastToHast = require('mdast-util-to-hast');
function mdastToHTML(nodes) {
function mdastToHTML(nodes, hastOptions = {}) {
if (!Array.isArray(nodes))
throw Error('mdastToHTML expects an array argument');
// - the 'nodes' are children, so first need embedding in a parent
return hastToHTML(mdastToHast(root(nodes), { allowDangerousHtml: true }), {
allowDangerousHtml: true
});
return hastToHTML(
mdastToHast(root(nodes), { allowDangerousHtml: true, ...hastOptions }),
{
allowDangerousHtml: true
}
);
}
module.exports = mdastToHTML;