mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(client,challenge-parser): update fill-in-the-blank to support Chinese (#63741)
This commit is contained in:
@@ -410,6 +410,7 @@ exports.createSchemaCustomization = ({ actions }) => {
|
|||||||
type FillInTheBlank {
|
type FillInTheBlank {
|
||||||
sentence: String
|
sentence: String
|
||||||
blanks: [Blank]
|
blanks: [Blank]
|
||||||
|
inputType: String
|
||||||
}
|
}
|
||||||
type Blank {
|
type Blank {
|
||||||
answer: String
|
answer: String
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export type Question = {
|
|||||||
export type FillInTheBlank = {
|
export type FillInTheBlank = {
|
||||||
sentence: string;
|
sentence: string;
|
||||||
blanks: MultipleChoiceAnswer[];
|
blanks: MultipleChoiceAnswer[];
|
||||||
|
inputType?: 'pinyin-tone' | 'pinyin-to-hanzi';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Fields = {
|
export type Fields = {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Spacer } from '@freecodecamp/ui';
|
import { Spacer } from '@freecodecamp/ui';
|
||||||
|
|
||||||
import { parseBlanks } from '../fill-in-the-blank/parse-blanks';
|
import { parseBlanks, parseAnswer } from '../fill-in-the-blank/parse-blanks';
|
||||||
import PrismFormatted from '../components/prism-formatted';
|
import PrismFormatted from '../components/prism-formatted';
|
||||||
import { FillInTheBlank } from '../../../redux/prop-types';
|
import { FillInTheBlank } from '../../../redux/prop-types';
|
||||||
import ChallengeHeading from './challenge-heading';
|
import ChallengeHeading from './challenge-heading';
|
||||||
@@ -16,6 +16,23 @@ type FillInTheBlankProps = {
|
|||||||
handleInputChange: (inputIndex: number, value: string) => void;
|
handleInputChange: (inputIndex: number, value: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AnswerText = ({ answer }: { answer: string }) => {
|
||||||
|
const parsedAnswer = parseAnswer(answer);
|
||||||
|
|
||||||
|
if (typeof parsedAnswer === 'string') {
|
||||||
|
return <span className='correct-blank-answer'>{parsedAnswer}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ruby className='correct-blank-answer'>
|
||||||
|
{parsedAnswer.hanzi}
|
||||||
|
<rp>(</rp>
|
||||||
|
<rt>{parsedAnswer.pinyin}</rt>
|
||||||
|
<rp>)</rp>
|
||||||
|
</ruby>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function FillInTheBlanks({
|
function FillInTheBlanks({
|
||||||
fillInTheBlank: { sentence, blanks },
|
fillInTheBlank: { sentence, blanks },
|
||||||
answersCorrect,
|
answersCorrect,
|
||||||
@@ -36,6 +53,17 @@ function FillInTheBlanks({
|
|||||||
return cls;
|
return cls;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAnswerLength = (answer: string): number => {
|
||||||
|
const parsedAnswer = parseAnswer(answer);
|
||||||
|
|
||||||
|
if (typeof parsedAnswer === 'string') {
|
||||||
|
return parsedAnswer.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This is a simplification. Revisit later to account for tones and spaces.
|
||||||
|
return parsedAnswer.pinyin.length;
|
||||||
|
};
|
||||||
|
|
||||||
const paragraphs = parseBlanks(sentence);
|
const paragraphs = parseBlanks(sentence);
|
||||||
const blankAnswers = blanks.map(b => b.answer);
|
const blankAnswers = blanks.map(b => b.answer);
|
||||||
|
|
||||||
@@ -55,25 +83,35 @@ function FillInTheBlanks({
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a blank is answered correctly, render the answer as part of the sentence.
|
if (type === 'hanzi-pinyin') {
|
||||||
if (type === 'blank' && answersCorrect[value] === true) {
|
const { hanzi, pinyin } = value;
|
||||||
return (
|
return (
|
||||||
<span key={j} className='correct-blank-answer'>
|
<ruby key={j}>
|
||||||
{blankAnswers[value]}
|
{hanzi}
|
||||||
</span>
|
<rp>(</rp>
|
||||||
|
<rt>{pinyin}</rt>
|
||||||
|
<rp>)</rp>
|
||||||
|
</ruby>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a blank is answered correctly, render the answer as part of the sentence.
|
||||||
|
if (type === 'blank' && answersCorrect[value] === true) {
|
||||||
|
return <AnswerText key={j} answer={blankAnswers[value]} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const answerLength = getAnswerLength(blankAnswers[value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
key={j}
|
key={j}
|
||||||
type='text'
|
type='text'
|
||||||
maxLength={blankAnswers[value].length + 3}
|
maxLength={answerLength + 3}
|
||||||
className={getInputClass(value)}
|
className={getInputClass(value)}
|
||||||
onChange={e =>
|
onChange={e =>
|
||||||
handleInputChange(node.value, e.target.value)
|
handleInputChange(node.value, e.target.value)
|
||||||
}
|
}
|
||||||
size={blankAnswers[value].length}
|
size={answerLength}
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
aria-label={t('learn.fill-in-the-blank.blank')}
|
aria-label={t('learn.fill-in-the-blank.blank')}
|
||||||
{...(answersCorrect[value] === false
|
{...(answersCorrect[value] === false
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { parseBlanks } from './parse-blanks';
|
import {
|
||||||
|
parseBlanks,
|
||||||
|
parseHanziPinyinPairs,
|
||||||
|
parseAnswer
|
||||||
|
} from './parse-blanks';
|
||||||
|
|
||||||
describe('parseBlanks', () => {
|
describe('parseBlanks', () => {
|
||||||
it('handles strings without blanks', () => {
|
it('handles strings without blanks', () => {
|
||||||
@@ -129,4 +133,221 @@ describe('parseBlanks', () => {
|
|||||||
expect(() => parseBlanks('<p>hello BLANK!</p>hello BLANK!')).toThrow();
|
expect(() => parseBlanks('<p>hello BLANK!</p>hello BLANK!')).toThrow();
|
||||||
expect(() => parseBlanks('hello BLANK!<p>hello</p>')).toThrow();
|
expect(() => parseBlanks('hello BLANK!<p>hello</p>')).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles Chinese with single BLANK', () => {
|
||||||
|
expect(
|
||||||
|
parseBlanks('<p>BLANK<ruby>好<rp>(</rp><rt>hǎo</rt><rp>)</rp></ruby></p>')
|
||||||
|
).toEqual([
|
||||||
|
[
|
||||||
|
{ type: 'blank', value: 0 },
|
||||||
|
{
|
||||||
|
type: 'hanzi-pinyin',
|
||||||
|
value: { hanzi: '好', pinyin: 'hǎo' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles Chinese without pinyin', () => {
|
||||||
|
expect(parseBlanks('<p>你BLANK好</p>')).toEqual([
|
||||||
|
[
|
||||||
|
{ type: 'text', value: '你' },
|
||||||
|
{ type: 'blank', value: 0 },
|
||||||
|
{ type: 'text', value: '好' }
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles Chinese with multiple BLANKs', () => {
|
||||||
|
expect(
|
||||||
|
parseBlanks(
|
||||||
|
'<p>BLANK<ruby>好<rp>(</rp><rt>hǎo</rt><rp>)</rp></ruby>,BLANK<ruby>是王华<rp>(</rp><rt>shì Wang Hua</rt><rp>)</rp></ruby></p>'
|
||||||
|
)
|
||||||
|
).toEqual([
|
||||||
|
[
|
||||||
|
{ type: 'blank', value: 0 },
|
||||||
|
{
|
||||||
|
type: 'hanzi-pinyin',
|
||||||
|
value: { hanzi: '好', pinyin: 'hǎo' }
|
||||||
|
},
|
||||||
|
{ type: 'text', value: ',' },
|
||||||
|
{ type: 'blank', value: 1 },
|
||||||
|
{
|
||||||
|
type: 'hanzi-pinyin',
|
||||||
|
value: { hanzi: '是王华', pinyin: 'shì Wang Hua' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles Chinese with multiple adjacent BLANKs', () => {
|
||||||
|
expect(
|
||||||
|
parseBlanks(
|
||||||
|
'<p>BLANK BLANK<ruby>好<rp>(</rp><rt>hǎo</rt><rp>)</rp></ruby></p>'
|
||||||
|
)
|
||||||
|
).toEqual([
|
||||||
|
[
|
||||||
|
{ type: 'blank', value: 0 },
|
||||||
|
{ type: 'text', value: ' ' },
|
||||||
|
{ type: 'blank', value: 1 },
|
||||||
|
{
|
||||||
|
type: 'hanzi-pinyin',
|
||||||
|
value: { hanzi: '好', pinyin: 'hǎo' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles Chinese with BLANK at the end', () => {
|
||||||
|
expect(
|
||||||
|
parseBlanks(
|
||||||
|
'<p><ruby>你好<rp>(</rp><rt>nǐ hǎo</rt><rp>)</rp></ruby>BLANK</p>'
|
||||||
|
)
|
||||||
|
).toEqual([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'hanzi-pinyin',
|
||||||
|
value: { hanzi: '你好', pinyin: 'nǐ hǎo' }
|
||||||
|
},
|
||||||
|
{ type: 'blank', value: 0 }
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles Chinese with spaces around BLANK', () => {
|
||||||
|
expect(
|
||||||
|
parseBlanks(
|
||||||
|
'<p><ruby>你<rp>(</rp><rt>nǐ</rt><rp>)</rp></ruby> BLANK <ruby>我<rp>(</rp><rt>wǒ</rt><rp>)</rp></ruby></p>'
|
||||||
|
)
|
||||||
|
).toEqual([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'hanzi-pinyin',
|
||||||
|
value: { hanzi: '你', pinyin: 'nǐ' }
|
||||||
|
},
|
||||||
|
{ type: 'text', value: ' ' },
|
||||||
|
{ type: 'blank', value: 0 },
|
||||||
|
{ type: 'text', value: ' ' },
|
||||||
|
{
|
||||||
|
type: 'hanzi-pinyin',
|
||||||
|
value: { hanzi: '我', pinyin: 'wǒ' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles Latin text adjacent to BLANK', () => {
|
||||||
|
expect(
|
||||||
|
parseBlanks(
|
||||||
|
'<p><ruby>我<rp>(</rp><rt>wǒ</rt><rp>)</rp></ruby> BLANK UI <ruby>设计师<rp>(</rp><rt>shè jì shī</rt><rp>)</rp></ruby> 。</p>'
|
||||||
|
)
|
||||||
|
).toEqual([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'hanzi-pinyin',
|
||||||
|
value: { hanzi: '我', pinyin: 'wǒ' }
|
||||||
|
},
|
||||||
|
{ type: 'text', value: ' ' },
|
||||||
|
{ type: 'blank', value: 0 },
|
||||||
|
{ type: 'text', value: ' UI ' },
|
||||||
|
{
|
||||||
|
type: 'hanzi-pinyin',
|
||||||
|
value: { hanzi: '设计师', pinyin: 'shè jì shī' }
|
||||||
|
},
|
||||||
|
{ type: 'text', value: ' 。' }
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles Chinese with multiple separate groups', () => {
|
||||||
|
expect(
|
||||||
|
parseBlanks(
|
||||||
|
'<p>BLANK<ruby>好<rp>(</rp><rt>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ǐ</rt><rp>)</rp></ruby>BLANK<ruby>什么名字<rp>(</rp><rt>shén me míng zi</rt><rp>)</rp></ruby>?</p>'
|
||||||
|
)
|
||||||
|
).toEqual([
|
||||||
|
[
|
||||||
|
{ type: 'blank', value: 0 },
|
||||||
|
{
|
||||||
|
type: 'hanzi-pinyin',
|
||||||
|
value: { hanzi: '好', pinyin: 'hǎo' }
|
||||||
|
},
|
||||||
|
{ type: 'text', value: ',' },
|
||||||
|
{
|
||||||
|
type: 'hanzi-pinyin',
|
||||||
|
value: { hanzi: '我是王华', pinyin: 'wǒ shì Wang Hua' }
|
||||||
|
},
|
||||||
|
{ type: 'text', value: ',' },
|
||||||
|
{
|
||||||
|
type: 'hanzi-pinyin',
|
||||||
|
value: { hanzi: '请问你', pinyin: 'qǐng wèn nǐ' }
|
||||||
|
},
|
||||||
|
{ type: 'blank', value: 1 },
|
||||||
|
{
|
||||||
|
type: 'hanzi-pinyin',
|
||||||
|
value: { hanzi: '什么名字', pinyin: 'shén me míng zi' }
|
||||||
|
},
|
||||||
|
{ type: 'text', value: '?' }
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles Chinese ruby with trailing punctuation', () => {
|
||||||
|
expect(
|
||||||
|
parseBlanks(
|
||||||
|
'<p><ruby>你是刘明吗<rp>(</rp><rt>nǐ shì Liu Ming ma</rt><rp>)</rp></ruby>?</p>'
|
||||||
|
)
|
||||||
|
).toEqual([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'hanzi-pinyin',
|
||||||
|
value: { hanzi: '你是刘明吗', pinyin: 'nǐ shì Liu Ming ma' }
|
||||||
|
},
|
||||||
|
{ type: 'text', value: '?' }
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseHanziPinyinPairs', () => {
|
||||||
|
it('parseHanziPinyinPairs returns array with one pair for well-formed input', () => {
|
||||||
|
const result = parseHanziPinyinPairs('你好 (nǐ hǎo)');
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]).toEqual({
|
||||||
|
hanzi: '你好',
|
||||||
|
pinyin: 'nǐ hǎo'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parseHanziPinyinPairs handles parentheses without a space', () => {
|
||||||
|
const result = parseHanziPinyinPairs('你好(nǐ hǎo)');
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]).toEqual({
|
||||||
|
hanzi: '你好',
|
||||||
|
pinyin: 'nǐ hǎo'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parseHanziPinyinPairs returns empty array for non-matching input', () => {
|
||||||
|
expect(parseHanziPinyinPairs('hello')).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parseAnswer returns parsed object when pattern matches', () => {
|
||||||
|
expect(parseAnswer('你好 (nǐ hǎo)')).toEqual({
|
||||||
|
hanzi: '你好',
|
||||||
|
pinyin: 'nǐ hǎo'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseAnswer', () => {
|
||||||
|
it('parseAnswer returns hanzi-pinyin string when pattern matches', () => {
|
||||||
|
expect(parseAnswer('你好(nǐ hǎo)')).toEqual({
|
||||||
|
hanzi: '你好',
|
||||||
|
pinyin: 'nǐ hǎo'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parseAnswer returns original string when pattern does not match', () => {
|
||||||
|
expect(parseAnswer('just some text')).toBe('just some text');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,48 @@
|
|||||||
type TextNode = { type: 'text'; value: string };
|
type PlainTextNode = {
|
||||||
|
type: 'text';
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hanzi/pinyin node representing an inline pronunciation pair
|
||||||
|
type HanziPinyinNode = {
|
||||||
|
type: 'hanzi-pinyin';
|
||||||
|
value: { hanzi: string; pinyin: string };
|
||||||
|
};
|
||||||
|
|
||||||
type BlankNode = { type: 'blank'; value: number };
|
type BlankNode = { type: 'blank'; value: number };
|
||||||
type ParagraphElement = TextNode | BlankNode;
|
|
||||||
|
type ParagraphElement = PlainTextNode | BlankNode | HanziPinyinNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses all hanzi-pinyin pairs from text
|
||||||
|
* @param text - Text potentially containing hanzi (pinyin) patterns
|
||||||
|
* @returns Array of parsed hanzi and pinyin pairs
|
||||||
|
*/
|
||||||
|
export function parseHanziPinyinPairs(
|
||||||
|
text: string
|
||||||
|
): Array<{ hanzi: string; pinyin: string }> {
|
||||||
|
const pairs: Array<{ hanzi: string; pinyin: string }> = [];
|
||||||
|
const regex = /([^()]+?)\s*\(([^)]+)\)/g;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
pairs.push({
|
||||||
|
hanzi: match[1].trim(),
|
||||||
|
pinyin: match[2].trim()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return pairs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseAnswer(
|
||||||
|
text: string
|
||||||
|
): { hanzi: string; pinyin: string } | string {
|
||||||
|
const pairs = parseHanziPinyinPairs(text);
|
||||||
|
const hanziPinyin = pairs.length === 1 ? pairs[0] : null;
|
||||||
|
|
||||||
|
return hanziPinyin || text;
|
||||||
|
}
|
||||||
|
|
||||||
export const parseBlanks = (text: string) => {
|
export const parseBlanks = (text: string) => {
|
||||||
const trimmed = text.trim();
|
const trimmed = text.trim();
|
||||||
@@ -19,27 +61,14 @@ to be wrapped in <p> tags`);
|
|||||||
|
|
||||||
const { paragraphs } = rawParagraphs.reduce(
|
const { paragraphs } = rawParagraphs.reduce(
|
||||||
(acc, p) => {
|
(acc, p) => {
|
||||||
const splitByBlank = p.split('BLANK');
|
const containsRuby = /<ruby>/.test(p);
|
||||||
|
const { elements, blankCount } = containsRuby
|
||||||
|
? parseChineseParagraph(p, acc.count)
|
||||||
|
: parsePlainParagraph(p, acc.count);
|
||||||
|
|
||||||
const parsedParagraph = splitByBlank
|
|
||||||
.map<ParagraphElement[]>((text, i) => [
|
|
||||||
{ type: 'text', value: text },
|
|
||||||
{ type: 'blank', value: acc.count + i }
|
|
||||||
])
|
|
||||||
.flat();
|
|
||||||
parsedParagraph.pop(); // remove last blank
|
|
||||||
|
|
||||||
const paragraph = parsedParagraph.filter(p => {
|
|
||||||
// remove empty strings
|
|
||||||
if (p.type === 'text') {
|
|
||||||
return p.value;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
count: acc.count + splitByBlank.length - 1,
|
count: acc.count + blankCount,
|
||||||
paragraphs: [...acc.paragraphs, paragraph]
|
paragraphs: [...acc.paragraphs, elements]
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{ count: 0, paragraphs: [] } as {
|
{ count: 0, paragraphs: [] } as {
|
||||||
@@ -50,3 +79,84 @@ to be wrapped in <p> tags`);
|
|||||||
|
|
||||||
return paragraphs;
|
return paragraphs;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a paragraph that contains ruby HTML elements (Chinese hanzi-pinyin)
|
||||||
|
* Handles multiple ruby elements separated by text and BLANK tokens
|
||||||
|
*/
|
||||||
|
function parseChineseParagraph(
|
||||||
|
paragraph: string,
|
||||||
|
startingBlankIndex: number
|
||||||
|
): { elements: ParagraphElement[]; blankCount: number } {
|
||||||
|
const elements: ParagraphElement[] = [];
|
||||||
|
let blankIndex = startingBlankIndex;
|
||||||
|
|
||||||
|
// First, split the paragraph on BLANK tokens so we can add blanks between segments
|
||||||
|
const segments = paragraph.split('BLANK');
|
||||||
|
|
||||||
|
for (let s = 0; s < segments.length; s++) {
|
||||||
|
const segment = segments[s];
|
||||||
|
|
||||||
|
// Split the segment into text and ruby parts. Capturing group keeps the ruby tags.
|
||||||
|
const parts = segment.split(/(<ruby>.*?<\/ruby>)/g).filter(Boolean);
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
if (part.startsWith('<ruby>')) {
|
||||||
|
const rubyMatch = part.match(
|
||||||
|
/^<ruby>([^<]+)<rp>\(<\/rp><rt>([^<]+)<\/rt><rp>\)<\/rp><\/ruby>$/
|
||||||
|
);
|
||||||
|
if (rubyMatch) {
|
||||||
|
elements.push({
|
||||||
|
type: 'hanzi-pinyin',
|
||||||
|
value: { hanzi: rubyMatch[1], pinyin: rubyMatch[2] }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (part) {
|
||||||
|
elements.push({ type: 'text', value: part });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// After each segment except the last, insert a blank node.
|
||||||
|
if (s < segments.length - 1) {
|
||||||
|
elements.push({ type: 'blank', value: blankIndex });
|
||||||
|
blankIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
elements,
|
||||||
|
blankCount: blankIndex - startingBlankIndex
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a plain (non-Chinese) paragraph
|
||||||
|
*/
|
||||||
|
function parsePlainParagraph(
|
||||||
|
paragraph: string,
|
||||||
|
startingBlankIndex: number
|
||||||
|
): { elements: ParagraphElement[]; blankCount: number } {
|
||||||
|
const splitByBlank = paragraph.split('BLANK');
|
||||||
|
|
||||||
|
const parsedParagraph = splitByBlank
|
||||||
|
.map<ParagraphElement[]>((text, i) => [
|
||||||
|
{ type: 'text', value: text },
|
||||||
|
{ type: 'blank', value: startingBlankIndex + i }
|
||||||
|
])
|
||||||
|
.flat();
|
||||||
|
|
||||||
|
// remove last blank inserted by the mapping
|
||||||
|
parsedParagraph.pop();
|
||||||
|
|
||||||
|
const elements = parsedParagraph.filter(p => {
|
||||||
|
if (p.type === 'text') {
|
||||||
|
return p.value;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
elements,
|
||||||
|
blankCount: splitByBlank.length - 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import { SceneSubject } from '../components/scene/scene-subject';
|
|||||||
import { getChallengePaths } from '../utils/challenge-paths';
|
import { getChallengePaths } from '../utils/challenge-paths';
|
||||||
import { isChallengeCompletedSelector } from '../redux/selectors';
|
import { isChallengeCompletedSelector } from '../redux/selectors';
|
||||||
import { replaceAppleQuotes } from '../../../utils/replace-apple-quotes';
|
import { replaceAppleQuotes } from '../../../utils/replace-apple-quotes';
|
||||||
|
import { parseHanziPinyinPairs } from './parse-blanks';
|
||||||
|
|
||||||
import './show.css';
|
import './show.css';
|
||||||
|
|
||||||
@@ -135,12 +136,27 @@ const ShowFillInTheBlank = ({
|
|||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
const blankAnswers = fillInTheBlank.blanks.map(b => b.answer);
|
const blankAnswers = fillInTheBlank.blanks.map(b => b.answer);
|
||||||
|
|
||||||
const newAnswersCorrect = userAnswers.map(
|
const newAnswersCorrect = userAnswers.map((userAnswer, i) => {
|
||||||
(userAnswer, i) =>
|
if (!userAnswer) return false;
|
||||||
!!userAnswer &&
|
|
||||||
replaceAppleQuotes(userAnswer.trim()).toLowerCase() ===
|
const answer = blankAnswers[i];
|
||||||
blankAnswers[i].toLowerCase()
|
const normalizedUserAnswer = replaceAppleQuotes(
|
||||||
);
|
userAnswer.trim()
|
||||||
|
).toLowerCase();
|
||||||
|
|
||||||
|
const pairs = parseHanziPinyinPairs(answer);
|
||||||
|
const hanziPinyin = pairs.length === 1 ? pairs[0] : null;
|
||||||
|
|
||||||
|
if (hanziPinyin) {
|
||||||
|
const { hanzi } = hanziPinyin;
|
||||||
|
// TODO: Implement full hanzi-pinyin validation logic
|
||||||
|
// https://github.com/freeCodeCamp/language-curricula/issues/18
|
||||||
|
return normalizedUserAnswer === hanzi;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedUserAnswer === answer.toLowerCase();
|
||||||
|
});
|
||||||
|
|
||||||
setAnswersCorrect(newAnswersCorrect);
|
setAnswersCorrect(newAnswersCorrect);
|
||||||
const hasWrongAnswer = newAnswersCorrect.some(a => a === false);
|
const hasWrongAnswer = newAnswersCorrect.some(a => a === false);
|
||||||
if (!hasWrongAnswer) {
|
if (!hasWrongAnswer) {
|
||||||
@@ -294,6 +310,7 @@ export const query = graphql`
|
|||||||
answer
|
answer
|
||||||
feedback
|
feedback
|
||||||
}
|
}
|
||||||
|
inputType
|
||||||
}
|
}
|
||||||
tests {
|
tests {
|
||||||
text
|
text
|
||||||
|
|||||||
+1
-1
@@ -54,4 +54,4 @@ That is part of the question, but not how she politely begins it.
|
|||||||
|
|
||||||
`请问 (qǐng wèn)` means "excuse me". It's often used at the start of a question to sound polite. For example:
|
`请问 (qǐng wèn)` means "excuse me". It's often used at the start of a question to sound polite. For example:
|
||||||
|
|
||||||
`请问你是刘明吗?(qǐng wèn nǐ shì Liu Ming ma)` – Excuse me, are you Liu Ming?
|
`请问你是刘明吗 (qǐng wèn nǐ shì Liu Ming ma)?` – Excuse me, are you Liu Ming?
|
||||||
|
|||||||
+3
-7
@@ -21,24 +21,20 @@ Listen to the audio and complete the sentence below.
|
|||||||
|
|
||||||
## --sentence--
|
## --sentence--
|
||||||
|
|
||||||
`你好,我是王华,请问BLANK叫什么名字?(nǐ hǎo wǒ shì Wang Hua qǐng wèn BLANK jiào shén me míng zi)`
|
`你好 (nǐ hǎo),我是王华 (wǒ shì Wang Hua),请问 (qǐng wèn) BLANK 叫什么名字 (jiào shén me míng zi)?`
|
||||||
|
|
||||||
## --blanks--
|
## --blanks--
|
||||||
|
|
||||||
`你`
|
`你 (nǐ)`
|
||||||
|
|
||||||
### --feedback--
|
### --feedback--
|
||||||
|
|
||||||
This word means "you" and refers to someone you are speaking to.
|
This word means "you" and refers to someone you are speaking to.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
`nǐ`
|
|
||||||
|
|
||||||
# --explanation--
|
# --explanation--
|
||||||
|
|
||||||
`你 (nǐ)` means "you". It's used to talk directly to another person. For example:
|
`你 (nǐ)` means "you". It's used to talk directly to another person. For example:
|
||||||
|
|
||||||
`你是刘明吗?(nǐ shì Liu Ming ma)` – Are you Liu Ming?
|
`你是刘明吗 (nǐ shì Liu Ming ma)?` – Are you Liu Ming?
|
||||||
|
|
||||||
You've learned how to use `我 (wǒ)` to refer to yourself. Both `我 (wǒ)` and `你 (nǐ)` are **personal pronouns**, which means they are used to refer to people.
|
You've learned how to use `我 (wǒ)` to refer to yourself. Both `我 (wǒ)` and `你 (nǐ)` are **personal pronouns**, which means they are used to refer to people.
|
||||||
|
|||||||
+3
-7
@@ -21,22 +21,18 @@ Listen to the audio and complete the sentence below.
|
|||||||
|
|
||||||
## --sentence--
|
## --sentence--
|
||||||
|
|
||||||
`你好,我是王华,请问你BLANK什么名字? (nǐ hǎo wǒ shì Wang Hua qǐng wèn nǐ BLANK shén me míng zi)`
|
`你好 (nǐ hǎo),我是王华 (wǒ shì Wang Hua),请问你 (qǐng wèn nǐ) BLANK 什么名字 (shén me míng zi)?`
|
||||||
|
|
||||||
## --blanks--
|
## --blanks--
|
||||||
|
|
||||||
`叫`
|
`叫 (jiào)`
|
||||||
|
|
||||||
### --feedback--
|
### --feedback--
|
||||||
|
|
||||||
This character means "to be called" or "to be named".
|
This character means "to be called" or "to be named".
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
`jiào`
|
|
||||||
|
|
||||||
# --explanation--
|
# --explanation--
|
||||||
|
|
||||||
`叫 (jiào)` means "to be called". It's often used after a subject to introduce a name. For example:
|
`叫 (jiào)` means "to be called". It's often used after a subject to introduce a name. For example:
|
||||||
|
|
||||||
`我叫王华。(wǒ jiào Wang Hua)` – I am called Wang Hua.
|
`我叫王华 (wǒ jiào Wang Hua)。` – I am called Wang Hua.
|
||||||
|
|||||||
+1
-1
@@ -52,4 +52,4 @@ She isn't asking where the person is from.
|
|||||||
|
|
||||||
# --explanation--
|
# --explanation--
|
||||||
|
|
||||||
`什么名字 (shén me míng zi)` means "what name". `你叫什么名字?(nǐ jiào shén me míng zi)` means "what is your name?". Wang Hua is asking for the other person's name.
|
`什么名字 (shén me míng zi)` means "what name". `你叫什么名字 (nǐ jiào shén me míng zi)?` means "what is your name?". Wang Hua is asking for the other person's name.
|
||||||
|
|||||||
@@ -228,7 +228,8 @@ const schema = Joi.object().keys({
|
|||||||
feedback: Joi.string().allow(null)
|
feedback: Joi.string().allow(null)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.required()
|
.required(),
|
||||||
|
inputType: Joi.string().valid('pinyin-tone', 'pinyin-to-hanzi').optional()
|
||||||
}),
|
}),
|
||||||
forumTopicId: Joi.number(),
|
forumTopicId: Joi.number(),
|
||||||
id: Joi.objectId().required(),
|
id: Joi.objectId().required(),
|
||||||
|
|||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
# --fillInTheBlank--
|
||||||
|
|
||||||
|
## --sentence--
|
||||||
|
|
||||||
|
`BLANK BLANK`
|
||||||
|
|
||||||
|
## --blanks--
|
||||||
|
|
||||||
|
`你 (nǐ)`
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# --fillInTheBlank--
|
||||||
|
|
||||||
|
## --sentence--
|
||||||
|
|
||||||
|
`我 (wǒ) BLANK UI 设计师 (shè jì shī) 。`
|
||||||
|
|
||||||
|
## --blanks--
|
||||||
|
|
||||||
|
`是 (shì)`
|
||||||
|
|
||||||
|
### --feedback--
|
||||||
|
|
||||||
|
Feedback text.
|
||||||
|
|
||||||
|
# --explanation--
|
||||||
|
|
||||||
|
Explanation text.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# --fillInTheBlank--
|
||||||
|
|
||||||
|
## --sentence--
|
||||||
|
|
||||||
|
`你好 (nǐ hǎo)`
|
||||||
|
|
||||||
|
## --blanks--
|
||||||
|
|
||||||
|
`你`
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# --fillInTheBlank--
|
||||||
|
|
||||||
|
## --sentence--
|
||||||
|
|
||||||
|
`BLANK hǎo`
|
||||||
|
|
||||||
|
## --blanks--
|
||||||
|
|
||||||
|
`nǐ`
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# --fillInTheBlank--
|
||||||
|
|
||||||
|
## --sentence--
|
||||||
|
|
||||||
|
`BLANK好`
|
||||||
|
|
||||||
|
## --blanks--
|
||||||
|
|
||||||
|
`你 (nǐ)`
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
# --fillInTheBlank--
|
||||||
|
|
||||||
|
## --sentence--
|
||||||
|
|
||||||
|
`BLANK 好 (hǎo) BLANK`
|
||||||
|
|
||||||
|
## --blanks--
|
||||||
|
|
||||||
|
`你`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
`nǐ`
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
lang: zh-CN
|
||||||
|
inputType: pinyin-to-hanzi
|
||||||
|
---
|
||||||
|
|
||||||
|
# --fillInTheBlank--
|
||||||
|
|
||||||
|
## --sentence--
|
||||||
|
|
||||||
|
`BLANK BLANK,BLANK 是王华 (shì Wang Hua),请问你 (qǐng wèn nǐ) BLANK 什么名字 (shén me míng zi)?`
|
||||||
|
|
||||||
|
## --blanks--
|
||||||
|
|
||||||
|
`你 (nǐ)`
|
||||||
|
|
||||||
|
### --feedback--
|
||||||
|
|
||||||
|
Feedback text containing `汉字 (hàn zì)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
`好 (hǎo)`
|
||||||
|
|
||||||
|
### --feedback--
|
||||||
|
|
||||||
|
This means "good" or "well".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
`我 (wǒ)`
|
||||||
|
|
||||||
|
### --feedback--
|
||||||
|
|
||||||
|
This means "I".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
`叫 (jiào)`
|
||||||
|
|
||||||
|
### --feedback--
|
||||||
|
|
||||||
|
This means "to be called".
|
||||||
|
|
||||||
|
# --explanation--
|
||||||
|
|
||||||
|
Explanation text containing `汉字 (hàn zì)`.
|
||||||
@@ -49,4 +49,6 @@ Feedback text.
|
|||||||
|
|
||||||
# --explanation--
|
# --explanation--
|
||||||
|
|
||||||
Wang Hua uses `请问 (qǐng wèn)` to politely start her question.
|
`我是 (wǒ shì) Web 开发者 (kāi fā zhě)。` – I am a web developer.
|
||||||
|
|
||||||
|
`你好 (nǐ hǎo),我是王华 (wǒ shì Wang Hua),请问你叫什么名字 (qǐng wèn nǐ jiào shén me míng zi)?` – Hello, I am Wang Hua, may I ask what your name is?
|
||||||
@@ -3,8 +3,10 @@ const find = require('unist-util-find');
|
|||||||
const visit = require('unist-util-visit');
|
const visit = require('unist-util-visit');
|
||||||
const { getSection } = require('./utils/get-section');
|
const { getSection } = require('./utils/get-section');
|
||||||
const getAllBefore = require('./utils/before-heading');
|
const getAllBefore = require('./utils/before-heading');
|
||||||
const mdastToHtml = require('./utils/mdast-to-html');
|
const {
|
||||||
|
createMdastToHtml,
|
||||||
|
parseHanziPinyinPairs
|
||||||
|
} = require('./utils/i18n-stringify');
|
||||||
const { splitOnThematicBreak } = require('./utils/split-on-thematic-break');
|
const { splitOnThematicBreak } = require('./utils/split-on-thematic-break');
|
||||||
|
|
||||||
const NOT_IN_PARAGRAPHS = `Each inline code block in the fillInTheBlank sentence section must in its own paragraph
|
const NOT_IN_PARAGRAPHS = `Each inline code block in the fillInTheBlank sentence section must in its own paragraph
|
||||||
@@ -40,19 +42,102 @@ function plugin() {
|
|||||||
if (fillInTheBlankNodes.length > 0) {
|
if (fillInTheBlankNodes.length > 0) {
|
||||||
const fillInTheBlankTree = root(fillInTheBlankNodes);
|
const fillInTheBlankTree = root(fillInTheBlankNodes);
|
||||||
|
|
||||||
validateBlanksCount(fillInTheBlankTree);
|
validateBlanksSectionCount(fillInTheBlankTree);
|
||||||
|
|
||||||
const sentenceNodes = getSection(fillInTheBlankTree, '--sentence--');
|
const sentenceNodes = getSection(fillInTheBlankTree, '--sentence--');
|
||||||
const blanksNodes = getSection(fillInTheBlankTree, '--blanks--');
|
const blanksNodes = getSection(fillInTheBlankTree, '--blanks--');
|
||||||
|
|
||||||
const fillInTheBlank = getfillInTheBlank(sentenceNodes, blanksNodes);
|
const lang = file.data.lang;
|
||||||
|
const inputType = file.data.inputType;
|
||||||
|
const toHtml = createMdastToHtml(lang);
|
||||||
|
|
||||||
file.data.fillInTheBlank = fillInTheBlank;
|
file.data.fillInTheBlank = getFillInTheBlank(sentenceNodes, blanksNodes);
|
||||||
|
|
||||||
|
function getFillInTheBlank(sentenceNodes, blanksNodes) {
|
||||||
|
const sentenceWithoutCodeBlocks = sentenceNodes.map(node => {
|
||||||
|
node.children.forEach(child => {
|
||||||
|
if (child.type === 'text' && child.value.trim() === '')
|
||||||
|
throw Error(NOT_IN_PARAGRAPHS);
|
||||||
|
if (child.type !== 'inlineCode') throw Error(NOT_IN_CODE_BLOCK);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For Chinese hanzi-pinyin, keep as inlineCode so handler generates ruby elements
|
||||||
|
if (lang === 'zh-CN') {
|
||||||
|
const hasChinesePairs = node.children.some(
|
||||||
|
child =>
|
||||||
|
child.type === 'inlineCode' &&
|
||||||
|
parseHanziPinyinPairs(child.value).length > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasChinesePairs) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert inlineCode to text for non-Chinese content
|
||||||
|
const children = node.children.map(child => ({
|
||||||
|
...child,
|
||||||
|
type: 'text'
|
||||||
|
}));
|
||||||
|
return { ...node, children };
|
||||||
|
});
|
||||||
|
|
||||||
|
const sentence = toHtml(sentenceWithoutCodeBlocks);
|
||||||
|
const blanks = getBlanks(blanksNodes);
|
||||||
|
|
||||||
|
if (!sentence)
|
||||||
|
throw Error('sentence is missing from fill in the blank');
|
||||||
|
if (!blanks) throw Error('blanks are missing from fill in the blank');
|
||||||
|
if (sentence.match(/BLANK/g).length !== blanks.length)
|
||||||
|
throw Error(`Number of BLANKs doesn't match the number of answers.`);
|
||||||
|
|
||||||
|
// For 'pinyin-to-hanzi' inputType, all answers must be of type 'hanzi-pinyin'.
|
||||||
|
// This validation ensures compatibility with the pinyin input in the UI,
|
||||||
|
// where users type pinyin and the system automatically converts it to hanzi
|
||||||
|
// if the input value matches the expected pinyin from the answer.
|
||||||
|
if (inputType === 'pinyin-to-hanzi') {
|
||||||
|
const allAnswersAreHanziPinyin = blanks.every(
|
||||||
|
blank => parseHanziPinyinPairs(blank.answer).length === 1
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!allAnswersAreHanziPinyin) {
|
||||||
|
throw Error(
|
||||||
|
`When inputType is 'pinyin-to-hanzi', all answers must be in 'hanzi (pinyin)' format.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { sentence, blanks, ...(inputType && { inputType }) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBlanks(blanksNodes) {
|
||||||
|
const blanksGroups = splitOnThematicBreak(blanksNodes);
|
||||||
|
|
||||||
|
return blanksGroups.map(blanksGroup => {
|
||||||
|
const blanksTree = root(blanksGroup);
|
||||||
|
const feedback = find(blanksTree, { value: '--feedback--' });
|
||||||
|
|
||||||
|
if (feedback) {
|
||||||
|
const blanksNodes = getAllBefore(blanksTree, '--feedback--');
|
||||||
|
const feedbackNodes = getSection(blanksTree, '--feedback--');
|
||||||
|
|
||||||
|
return {
|
||||||
|
answer: blanksNodes[0].children[0].value,
|
||||||
|
feedback: toHtml(feedbackNodes)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
answer: blanksGroup[0].children[0].value,
|
||||||
|
feedback: null
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateBlanksCount(fillInTheBlankTree) {
|
function validateBlanksSectionCount(fillInTheBlankTree) {
|
||||||
let blanksCount = 0;
|
let blanksCount = 0;
|
||||||
visit(fillInTheBlankTree, { value: '--blanks--' }, () => {
|
visit(fillInTheBlankTree, { value: '--blanks--' }, () => {
|
||||||
blanksCount++;
|
blanksCount++;
|
||||||
@@ -64,49 +149,4 @@ function validateBlanksCount(fillInTheBlankTree) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getfillInTheBlank(sentenceNodes, blanksNodes) {
|
|
||||||
const sentenceWithoutCodeBlocks = sentenceNodes.map(node => {
|
|
||||||
node.children.forEach(child => {
|
|
||||||
if (child.type === 'text' && child.value.trim() === '')
|
|
||||||
throw Error(NOT_IN_PARAGRAPHS);
|
|
||||||
if (child.type !== 'inlineCode') throw Error(NOT_IN_CODE_BLOCK);
|
|
||||||
});
|
|
||||||
|
|
||||||
const children = node.children.map(child => ({ ...child, type: 'text' }));
|
|
||||||
return { ...node, children };
|
|
||||||
});
|
|
||||||
const sentence = mdastToHtml(sentenceWithoutCodeBlocks);
|
|
||||||
const blanks = getBlanks(blanksNodes);
|
|
||||||
|
|
||||||
if (!sentence) throw Error('sentence is missing from fill in the blank');
|
|
||||||
if (!blanks) throw Error('blanks are missing from fill in the blank');
|
|
||||||
if (sentence.match(/BLANK/g).length !== blanks.length)
|
|
||||||
throw Error(
|
|
||||||
`Number of underscores in sentence doesn't match the number of blanks`
|
|
||||||
);
|
|
||||||
|
|
||||||
return { sentence, blanks };
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBlanks(blanksNodes) {
|
|
||||||
const blanksGroups = splitOnThematicBreak(blanksNodes);
|
|
||||||
|
|
||||||
return blanksGroups.map(blanksGroup => {
|
|
||||||
const blanksTree = root(blanksGroup);
|
|
||||||
const feedback = find(blanksTree, { value: '--feedback--' });
|
|
||||||
|
|
||||||
if (feedback) {
|
|
||||||
const blanksNodes = getAllBefore(blanksTree, '--feedback--');
|
|
||||||
const feedbackNodes = getSection(blanksTree, '--feedback--');
|
|
||||||
|
|
||||||
return {
|
|
||||||
answer: blanksNodes[0].children[0].value,
|
|
||||||
feedback: mdastToHtml(feedbackNodes)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { answer: blanksGroup[0].children[0].value, feedback: null };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = plugin;
|
module.exports = plugin;
|
||||||
|
|||||||
@@ -8,7 +8,13 @@ describe('fill-in-the-blanks plugin', () => {
|
|||||||
mockFillInTheBlankTwoSentencesAST,
|
mockFillInTheBlankTwoSentencesAST,
|
||||||
mockFillInTheBlankBadSentence,
|
mockFillInTheBlankBadSentence,
|
||||||
mockFillInTheBlankBadParagraph,
|
mockFillInTheBlankBadParagraph,
|
||||||
mockFillInTheBlankMultipleBlanks;
|
mockFillInTheBlankMultipleBlanks,
|
||||||
|
mockChineseFillInTheBlankAST,
|
||||||
|
mockChineseFillInTheBlankNoPinyinAST,
|
||||||
|
mockChineseFillInTheBlankNoHanziAST,
|
||||||
|
mockChineseFillInTheBlankWrongAnswerFormatAST,
|
||||||
|
mockChineseFillInTheBlankBlankAnswerMismatchAST,
|
||||||
|
mockChineseFillInTheBlankLatinAST;
|
||||||
const plugin = addFillInTheBlankQuestion();
|
const plugin = addFillInTheBlankQuestion();
|
||||||
let file = { data: {} };
|
let file = { data: {} };
|
||||||
|
|
||||||
@@ -29,6 +35,24 @@ describe('fill-in-the-blanks plugin', () => {
|
|||||||
mockFillInTheBlankMultipleBlanks = await parseFixture(
|
mockFillInTheBlankMultipleBlanks = await parseFixture(
|
||||||
'with-fill-in-the-blank-many-blanks.md'
|
'with-fill-in-the-blank-many-blanks.md'
|
||||||
);
|
);
|
||||||
|
mockChineseFillInTheBlankAST = await parseFixture(
|
||||||
|
'with-chinese-fill-in-the-blank.md'
|
||||||
|
);
|
||||||
|
mockChineseFillInTheBlankNoPinyinAST = await parseFixture(
|
||||||
|
'with-chinese-fill-in-the-blank-no-pinyin.md'
|
||||||
|
);
|
||||||
|
mockChineseFillInTheBlankNoHanziAST = await parseFixture(
|
||||||
|
'with-chinese-fill-in-the-blank-no-hanzi.md'
|
||||||
|
);
|
||||||
|
mockChineseFillInTheBlankWrongAnswerFormatAST = await parseFixture(
|
||||||
|
'with-chinese-fill-in-the-blank-wrong-answer-format.md'
|
||||||
|
);
|
||||||
|
mockChineseFillInTheBlankBlankAnswerMismatchAST = await parseFixture(
|
||||||
|
'with-chinese-fill-in-the-blank-blank-answer-mismatch.md'
|
||||||
|
);
|
||||||
|
mockChineseFillInTheBlankLatinAST = await parseFixture(
|
||||||
|
'with-chinese-fill-in-the-blank-latin.md'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -55,15 +79,15 @@ describe('fill-in-the-blanks plugin', () => {
|
|||||||
expect(Array.isArray(testObject.blanks)).toBe(true);
|
expect(Array.isArray(testObject.blanks)).toBe(true);
|
||||||
expect(testObject.blanks.length).toBe(3);
|
expect(testObject.blanks.length).toBe(3);
|
||||||
expect(testObject.blanks[0]).toHaveProperty('answer');
|
expect(testObject.blanks[0]).toHaveProperty('answer');
|
||||||
expect(typeof testObject.blanks[0].answer).toBe('string');
|
expect(testObject.blanks[0].answer).toEqual('are');
|
||||||
expect(testObject.blanks[0]).toHaveProperty('feedback');
|
expect(testObject.blanks[0]).toHaveProperty('feedback');
|
||||||
expect(typeof testObject.blanks[0].feedback).toBe('string');
|
expect(typeof testObject.blanks[0].feedback).toBe('string');
|
||||||
expect(testObject.blanks[1]).toHaveProperty('answer');
|
expect(testObject.blanks[1]).toHaveProperty('answer');
|
||||||
expect(typeof testObject.blanks[1].answer).toBe('string');
|
expect(testObject.blanks[1].answer).toEqual('right');
|
||||||
expect(testObject.blanks[1]).toHaveProperty('feedback');
|
expect(testObject.blanks[1]).toHaveProperty('feedback');
|
||||||
expect(typeof testObject.blanks[1].feedback).toBe('string');
|
expect(typeof testObject.blanks[1].feedback).toBe('string');
|
||||||
expect(testObject.blanks[2]).toHaveProperty('answer');
|
expect(testObject.blanks[2]).toHaveProperty('answer');
|
||||||
expect(typeof testObject.blanks[2].answer).toBe('string');
|
expect(testObject.blanks[2].answer).toEqual('Nice');
|
||||||
expect(testObject.blanks[2]).toHaveProperty('feedback');
|
expect(testObject.blanks[2]).toHaveProperty('feedback');
|
||||||
expect(testObject.blanks[2].feedback).toBeNull();
|
expect(testObject.blanks[2].feedback).toBeNull();
|
||||||
});
|
});
|
||||||
@@ -167,4 +191,86 @@ Example of good formatting:
|
|||||||
'<p>The verb <code>to be</code> is an irregular verb. When conjugated with the pronoun <code>you</code>, <code>be</code> becomes <code>are</code>. For example: <code>You are an English learner.</code></p>'
|
'<p>The verb <code>to be</code> is an irregular verb. When conjugated with the pronoun <code>you</code>, <code>be</code> becomes <code>are</code>. For example: <code>You are an English learner.</code></p>'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should parse Chinese fill-in-the-blank sentence and answer correctly if they are in `hanzi (pinyin)` format', () => {
|
||||||
|
file.data.lang = 'zh-CN';
|
||||||
|
file.data.inputType = 'pinyin-to-hanzi';
|
||||||
|
plugin(mockChineseFillInTheBlankAST, file);
|
||||||
|
const testObject = file.data.fillInTheBlank;
|
||||||
|
|
||||||
|
expect(testObject.inputType).toBe('pinyin-to-hanzi');
|
||||||
|
|
||||||
|
expect(testObject.sentence).toBe(
|
||||||
|
'<p>BLANK BLANK,BLANK <ruby>是王华<rp>(</rp><rt>shì Wang Hua</rt><rp>)</rp></ruby>,<ruby>请问你<rp>(</rp><rt>qǐng wèn nǐ</rt><rp>)</rp></ruby> BLANK <ruby>什么名字<rp>(</rp><rt>shén me míng zi</rt><rp>)</rp></ruby>?</p>'
|
||||||
|
);
|
||||||
|
expect(testObject.blanks.length).toBe(4);
|
||||||
|
|
||||||
|
expect(testObject.blanks[0].answer).toEqual('你 (nǐ)');
|
||||||
|
expect(testObject.blanks[0].feedback).toBe(
|
||||||
|
'<p>Feedback text containing <ruby>汉字<rp>(</rp><rt>hàn zì</rt><rp>)</rp></ruby>.</p>'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(testObject.blanks[1].answer).toEqual('好 (hǎo)');
|
||||||
|
expect(testObject.blanks[1].feedback).toBe(
|
||||||
|
'<p>This means "good" or "well".</p>'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(testObject.blanks[2].answer).toEqual('我 (wǒ)');
|
||||||
|
expect(testObject.blanks[2].feedback).toBe('<p>This means "I".</p>');
|
||||||
|
|
||||||
|
expect(testObject.blanks[3].answer).toEqual('叫 (jiào)');
|
||||||
|
expect(testObject.blanks[3].feedback).toBe(
|
||||||
|
'<p>This means "to be called".</p>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return sentence as plain text when sentence does not contain pinyin', () => {
|
||||||
|
file.data.lang = 'zh-CN';
|
||||||
|
plugin(mockChineseFillInTheBlankNoPinyinAST, file);
|
||||||
|
const testObject = file.data.fillInTheBlank;
|
||||||
|
|
||||||
|
expect(testObject.sentence).toBe('<p>BLANK好</p>');
|
||||||
|
expect(testObject.blanks[0].answer).toEqual('你 (nǐ)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return sentence as plain text when sentence does not contain hanzi', () => {
|
||||||
|
file.data.lang = 'zh-CN';
|
||||||
|
plugin(mockChineseFillInTheBlankNoHanziAST, file);
|
||||||
|
const testObject = file.data.fillInTheBlank;
|
||||||
|
|
||||||
|
expect(testObject.sentence).toBe('<p>BLANK hǎo</p>');
|
||||||
|
expect(testObject.blanks[0].answer).toEqual('nǐ');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw if the number of blanks in the sentence doesn't match the number of answers", () => {
|
||||||
|
file.data.lang = 'zh-CN';
|
||||||
|
expect(() => {
|
||||||
|
plugin(mockChineseFillInTheBlankBlankAnswerMismatchAST, file);
|
||||||
|
}).toThrow(`Number of BLANKs doesn't match the number of answers.`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when inputType is pinyin-to-hanzi but answer is not in hanzi-pinyin format', () => {
|
||||||
|
file.data.lang = 'zh-CN';
|
||||||
|
file.data.inputType = 'pinyin-to-hanzi';
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
plugin(mockChineseFillInTheBlankWrongAnswerFormatAST, file);
|
||||||
|
}).toThrow(
|
||||||
|
"When inputType is 'pinyin-to-hanzi', all answers must be in 'hanzi (pinyin)' format."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should separate BLANK and adjacent Latin text in Chinese sentences', () => {
|
||||||
|
file.data.lang = 'zh-CN';
|
||||||
|
plugin(mockChineseFillInTheBlankLatinAST, file);
|
||||||
|
const testObject = file.data.fillInTheBlank;
|
||||||
|
|
||||||
|
expect(testObject.sentence).toBe(
|
||||||
|
'<p><ruby>我<rp>(</rp><rt>wǒ</rt><rp>)</rp></ruby> BLANK UI <ruby>设计师<rp>(</rp><rt>shè jì shī</rt><rp>)</rp></ruby> 。</p>'
|
||||||
|
);
|
||||||
|
expect(testObject.blanks.length).toBe(1);
|
||||||
|
|
||||||
|
expect(testObject.blanks[0].answer).toEqual('是 (shì)');
|
||||||
|
expect(testObject.blanks[0].feedback).toBe('<p>Feedback text.</p>');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ describe('add-text', () => {
|
|||||||
'<section id="instructions">\n<p>Instructions containing <ruby>汉字<rp>(</rp><rt>hàn zì</rt><rp>)</rp></ruby>.</p>\n</section>'
|
'<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(
|
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>'
|
'<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>'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,61 +1,94 @@
|
|||||||
const mdastToHTML = require('./mdast-to-html');
|
const mdastToHTML = require('./mdast-to-html');
|
||||||
|
|
||||||
/**
|
// Captures hanzi (pinyin) pairs (hanzi, optional whitespace, then pinyin parentheses)
|
||||||
* Parses Chinese text in format: hanzi (pinyin)
|
const HANZI_PINYIN_PAIR = '([\u4e00-\u9fff]+)\\s*\\(([^)]+)\\)';
|
||||||
* @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) {
|
// Matches the BLANK placeholder
|
||||||
return null;
|
const BLANK_TOKEN = 'BLANK';
|
||||||
|
|
||||||
|
// Matches Chinese and English punctuation
|
||||||
|
const PUNCTUATION = '[,。?!!?,;:;:、]+';
|
||||||
|
|
||||||
|
// Matches Latin text with spaces
|
||||||
|
const OTHER_TEXT = '([a-zA-Z\\s]+)';
|
||||||
|
|
||||||
|
const HANZI_PINYIN_REGEX = new RegExp(
|
||||||
|
`${HANZI_PINYIN_PAIR}|${BLANK_TOKEN}|${PUNCTUATION}|${OTHER_TEXT}`,
|
||||||
|
'g'
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses all hanzi-pinyin pairs from text
|
||||||
|
* @param {string} text - Text potentially containing multiple hanzi (pinyin) patterns
|
||||||
|
* @returns {Array<{hanzi: string, pinyin: string}>} Array of parsed pairs
|
||||||
|
*/
|
||||||
|
function parseHanziPinyinPairs(text) {
|
||||||
|
const pairs = [];
|
||||||
|
const regex = new RegExp(HANZI_PINYIN_REGEX);
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
if (match[1] && match[2]) {
|
||||||
|
pairs.push({
|
||||||
|
hanzi: match[1].trim(),
|
||||||
|
pinyin: match[2].trim()
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return pairs;
|
||||||
hanzi: match[1].trim(),
|
|
||||||
pinyin: match[2].trim()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom handler for Chinese inline code to render as ruby elements
|
* Custom handler for Chinese inline code to render as ruby elements
|
||||||
|
* Matches hanzi-pinyin pairs, BLANK, and punctuation as separate elements
|
||||||
* @param {object} state - The state object from mdast-util-to-hast
|
* @param {object} state - The state object from mdast-util-to-hast
|
||||||
* @param {object} node - The inlineCode node
|
* @param {object} node - The inlineCode node
|
||||||
* @returns {object} Hast element node
|
* @returns {object|Array<object>} Hast element node or array of nodes
|
||||||
*/
|
*/
|
||||||
function chineseInlineCodeHandler(state, node) {
|
function chineseInlineCodeHandler(state, node) {
|
||||||
const parsed = parseChinesePattern(node.value);
|
const rubyPairs = parseHanziPinyinPairs(node.value);
|
||||||
|
|
||||||
if (parsed) {
|
if (rubyPairs.length > 0) {
|
||||||
return {
|
const matches = [...node.value.matchAll(HANZI_PINYIN_REGEX)];
|
||||||
type: 'element',
|
const nodes = matches.map(fullMatch => {
|
||||||
tagName: 'ruby',
|
if (fullMatch[1] && fullMatch[2]) {
|
||||||
properties: {},
|
return {
|
||||||
children: [
|
|
||||||
{ type: 'text', value: parsed.hanzi },
|
|
||||||
{
|
|
||||||
type: 'element',
|
type: 'element',
|
||||||
tagName: 'rp',
|
tagName: 'ruby',
|
||||||
properties: {},
|
properties: {},
|
||||||
children: [{ type: 'text', value: '(' }]
|
children: [
|
||||||
},
|
{ type: 'text', value: fullMatch[1].trim() },
|
||||||
{
|
{
|
||||||
type: 'element',
|
type: 'element',
|
||||||
tagName: 'rt',
|
tagName: 'rp',
|
||||||
properties: {},
|
properties: {},
|
||||||
children: [{ type: 'text', value: parsed.pinyin }]
|
children: [{ type: 'text', value: '(' }]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'element',
|
type: 'element',
|
||||||
tagName: 'rp',
|
tagName: 'rt',
|
||||||
properties: {},
|
properties: {},
|
||||||
children: [{ type: 'text', value: ')' }]
|
children: [{ type: 'text', value: fullMatch[2].trim() }]
|
||||||
}
|
},
|
||||||
]
|
{
|
||||||
};
|
type: 'element',
|
||||||
|
tagName: 'rp',
|
||||||
|
properties: {},
|
||||||
|
children: [{ type: 'text', value: ')' }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other captures (BLANK, punctuation, other text including spaces) should preserve exactly
|
||||||
|
return { type: 'text', value: fullMatch[0] };
|
||||||
|
});
|
||||||
|
|
||||||
|
return nodes.length === 1 ? nodes[0] : nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If static text, return code
|
||||||
return {
|
return {
|
||||||
type: 'element',
|
type: 'element',
|
||||||
// TODO: change this to span
|
// TODO: change this to span
|
||||||
@@ -75,4 +108,7 @@ const rubyOptions = {
|
|||||||
const createMdastToHtml = lang =>
|
const createMdastToHtml = lang =>
|
||||||
lang == 'zh-CN' ? x => mdastToHTML(x, rubyOptions) : mdastToHTML;
|
lang == 'zh-CN' ? x => mdastToHTML(x, rubyOptions) : mdastToHTML;
|
||||||
|
|
||||||
module.exports = { parseChinesePattern, createMdastToHtml };
|
module.exports = {
|
||||||
|
parseHanziPinyinPairs,
|
||||||
|
createMdastToHtml
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,44 +1,56 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { createMdastToHtml, parseChinesePattern } from './i18n-stringify';
|
import { createMdastToHtml, parseHanziPinyinPairs } from './i18n-stringify';
|
||||||
|
|
||||||
describe('parseChinesePattern', () => {
|
describe('parseHanziPinyinPairs', () => {
|
||||||
it('should parse Chinese text with hanzi and pinyin', () => {
|
it('should parse single hanzi-pinyin pair', () => {
|
||||||
const result = parseChinesePattern('你好 (nǐ hǎo)');
|
const withSpaceSeparator = parseHanziPinyinPairs('你好 (nǐ hǎo)');
|
||||||
expect(result).toEqual({
|
|
||||||
|
expect(withSpaceSeparator).toHaveLength(1);
|
||||||
|
expect(withSpaceSeparator[0]).toMatchObject({
|
||||||
|
hanzi: '你好',
|
||||||
|
pinyin: 'nǐ hǎo'
|
||||||
|
});
|
||||||
|
|
||||||
|
const withoutSpaceSeparator = parseHanziPinyinPairs('你好(nǐ hǎo)');
|
||||||
|
|
||||||
|
expect(withoutSpaceSeparator).toHaveLength(1);
|
||||||
|
expect(withoutSpaceSeparator[0]).toMatchObject({
|
||||||
hanzi: '你好',
|
hanzi: '你好',
|
||||||
pinyin: 'nǐ hǎo'
|
pinyin: 'nǐ hǎo'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle text without spaces before parentheses', () => {
|
it('should parse multiple hanzi-pinyin pairs', () => {
|
||||||
const result = parseChinesePattern('你好(nǐ hǎo)');
|
const withSpaceSeparator = parseHanziPinyinPairs(
|
||||||
expect(result).toEqual({
|
'你好 (nǐ hǎo),我是王华 (wǒ shì Wang Hua)'
|
||||||
|
);
|
||||||
|
expect(withSpaceSeparator).toHaveLength(2);
|
||||||
|
expect(withSpaceSeparator[0]).toMatchObject({
|
||||||
hanzi: '你好',
|
hanzi: '你好',
|
||||||
pinyin: 'nǐ hǎo'
|
pinyin: 'nǐ hǎo'
|
||||||
});
|
});
|
||||||
});
|
expect(withSpaceSeparator[1]).toMatchObject({
|
||||||
|
hanzi: '我是王华',
|
||||||
|
pinyin: 'wǒ shì Wang Hua'
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle text with multiple spaces', () => {
|
const withoutSpaceSeparator = parseHanziPinyinPairs(
|
||||||
const result = parseChinesePattern('你好 (nǐ hǎo)');
|
'你好(nǐ hǎo),我是王华(wǒ shì Wang Hua)'
|
||||||
expect(result).toEqual({
|
);
|
||||||
|
expect(withoutSpaceSeparator).toHaveLength(2);
|
||||||
|
expect(withoutSpaceSeparator[0]).toMatchObject({
|
||||||
hanzi: '你好',
|
hanzi: '你好',
|
||||||
pinyin: 'nǐ hǎo'
|
pinyin: 'nǐ hǎo'
|
||||||
});
|
});
|
||||||
|
expect(withoutSpaceSeparator[1]).toMatchObject({
|
||||||
|
hanzi: '我是王华',
|
||||||
|
pinyin: 'wǒ shì Wang Hua'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return null for text without parentheses', () => {
|
it('should return empty array for text without pairs', () => {
|
||||||
const result = parseChinesePattern('你好');
|
const result = parseHanziPinyinPairs('你好');
|
||||||
expect(result).toBeNull();
|
expect(result).toHaveLength(0);
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -99,6 +111,93 @@ describe('createMdastToHtml', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should render BLANK tokens and punctuation marks as plain text', () => {
|
||||||
|
const toHtml = createMdastToHtml('zh-CN');
|
||||||
|
const withoutSpacesAroundBlanks = [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'inlineCode',
|
||||||
|
value:
|
||||||
|
'你好 (nǐ hǎo),BLANK是王华 (shì Wang Hua),请问你 (qǐng wèn nǐ)BLANK什么名字 (shén me míng zi)?'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
expect(toHtml(withoutSpacesAroundBlanks)).toBe(
|
||||||
|
'<p><ruby>你好<rp>(</rp><rt>nǐ hǎo</rt><rp>)</rp></ruby>,BLANK<ruby>是王华<rp>(</rp><rt>shì Wang Hua</rt><rp>)</rp></ruby>,<ruby>请问你<rp>(</rp><rt>qǐng wèn nǐ</rt><rp>)</rp></ruby>BLANK<ruby>什么名字<rp>(</rp><rt>shén me míng zi</rt><rp>)</rp></ruby>?</p>'
|
||||||
|
);
|
||||||
|
|
||||||
|
const withSpacesAroundBlanks = [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'inlineCode',
|
||||||
|
value:
|
||||||
|
'你好 (nǐ hǎo), BLANK 是王华 (shì Wang Hua),请问你 (qǐng wèn nǐ) BLANK 什么名字 (shén me míng zi)?'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
expect(toHtml(withSpacesAroundBlanks)).toBe(
|
||||||
|
'<p><ruby>你好<rp>(</rp><rt>nǐ hǎo</rt><rp>)</rp></ruby>, BLANK <ruby>是王华<rp>(</rp><rt>shì Wang Hua</rt><rp>)</rp></ruby>,<ruby>请问你<rp>(</rp><rt>qǐng wèn nǐ</rt><rp>)</rp></ruby> BLANK <ruby>什么名字<rp>(</rp><rt>shén me míng zi</rt><rp>)</rp></ruby>?</p>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render Latin words as plain text while applying ruby to hanzi-pinyin pairs', () => {
|
||||||
|
const toHtml = createMdastToHtml('zh-CN');
|
||||||
|
const nodes = [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'inlineCode',
|
||||||
|
value: '我是 (wǒ shì) UI 设计师 (shè jì shī)'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const actual = toHtml(nodes);
|
||||||
|
expect(actual).toBe(
|
||||||
|
'<p><ruby>我是<rp>(</rp><rt>wǒ shì</rt><rp>)</rp></ruby> UI <ruby>设计师<rp>(</rp><rt>shè jì shī</rt><rp>)</rp></ruby></p>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle BLANK token and Latin word mix', () => {
|
||||||
|
const toHtml = createMdastToHtml('zh-CN');
|
||||||
|
const nodes = [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'inlineCode',
|
||||||
|
value: '我 (wǒ) BLANK UI 设计师 (shè jì shī)'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const actual = toHtml(nodes);
|
||||||
|
expect(actual).toBe(
|
||||||
|
'<p><ruby>我<rp>(</rp><rt>wǒ</rt><rp>)</rp></ruby> BLANK UI <ruby>设计师<rp>(</rp><rt>shè jì shī</rt><rp>)</rp></ruby></p>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render multiple adjacent BLANK tokens in Chinese sentence', () => {
|
||||||
|
const toHtml = createMdastToHtml('zh-CN');
|
||||||
|
const nodes = [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
children: [{ type: 'inlineCode', value: 'BLANK BLANK,你好 (nǐ hǎo)' }]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const actual = toHtml(nodes);
|
||||||
|
expect(actual).toBe(
|
||||||
|
'<p>BLANK BLANK,<ruby>你好<rp>(</rp><rt>nǐ hǎo</rt><rp>)</rp></ruby></p>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should fallback to code element if pattern does not match', () => {
|
it('should fallback to code element if pattern does not match', () => {
|
||||||
const toHtml = createMdastToHtml('zh-CN');
|
const toHtml = createMdastToHtml('zh-CN');
|
||||||
const nodes = [
|
const nodes = [
|
||||||
@@ -126,4 +225,16 @@ describe('createMdastToHtml', () => {
|
|||||||
const actual = toHtml(nodes);
|
const actual = toHtml(nodes);
|
||||||
expect(actual).toBe('<p><code>请问 (qǐng wèn)</code></p>');
|
expect(actual).toBe('<p><code>请问 (qǐng wèn)</code></p>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should render as regular code when lang is not defined', () => {
|
||||||
|
const toHtml = createMdastToHtml();
|
||||||
|
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>');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user