chore: import upstream snapshot with attribution
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
CD - Docker - GHCR Images / Build and Push Images (push) Has been cancelled
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
CD - Docker - GHCR Images / Build and Push Images (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
const { root } = require('mdast-builder');
|
||||
const { getSection, getAllSections } = require('./utils/get-section');
|
||||
const {
|
||||
createMdastToHtml,
|
||||
parseHanziPinyinPairs
|
||||
} = 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, audioNodes) {
|
||||
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');
|
||||
|
||||
const questionData = { text, distractors, answer };
|
||||
|
||||
// Extract audio data if present
|
||||
if (audioNodes.length > 0) {
|
||||
// Audio should be in a JSON code block
|
||||
if (audioNodes[0].type !== 'code' || audioNodes[0].lang !== 'json') {
|
||||
throw Error('--audio-- section must contain a ```json code block');
|
||||
}
|
||||
|
||||
try {
|
||||
const audioData = JSON.parse(audioNodes[0].value);
|
||||
|
||||
// Validate structure
|
||||
if (!audioData.audio || !audioData.audio.filename) {
|
||||
throw Error('--audio-- section must contain audio.filename');
|
||||
}
|
||||
|
||||
if (!audioData.transcript || !Array.isArray(audioData.transcript)) {
|
||||
throw Error(
|
||||
'--audio-- section must contain transcript as an array'
|
||||
);
|
||||
}
|
||||
|
||||
if (audioData.transcript.length === 0) {
|
||||
throw Error('--audio-- section transcript array cannot be empty');
|
||||
}
|
||||
|
||||
// Validate each transcript line
|
||||
audioData.transcript.forEach((line, index) => {
|
||||
if (!line.character || !line.text) {
|
||||
throw Error(
|
||||
`--audio-- transcript line ${index} must have character and text properties`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert hanzi-pinyin pairs in transcript text to HTML ruby elements
|
||||
audioData.transcript = audioData.transcript.map(line => {
|
||||
if (parseHanziPinyinPairs(line.text).length > 0) {
|
||||
const nodes = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ type: 'inlineCode', value: line.text }]
|
||||
}
|
||||
];
|
||||
const html = toHtml(nodes);
|
||||
const innerHtml = html.replace(/^<p>|<\/p>$/g, '');
|
||||
return { ...line, text: innerHtml };
|
||||
}
|
||||
return line;
|
||||
});
|
||||
|
||||
questionData.audioData = audioData;
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
throw Error('--audio-- section must contain valid JSON');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return questionData;
|
||||
}
|
||||
|
||||
if (quizzesNodes.length > 0) {
|
||||
const compiledQuizzes = [];
|
||||
const quizSections = getAllSections(root(quizzesNodes), `--quiz--`);
|
||||
|
||||
if (quizSections.length === 0) {
|
||||
throw Error(
|
||||
'The --quizzes-- section should contain at least one quiz.'
|
||||
);
|
||||
}
|
||||
|
||||
quizSections.forEach(quizNodes => {
|
||||
const quizQuestions = [];
|
||||
const questionTrees = getAllSections(root(quizNodes), `--question--`);
|
||||
|
||||
if (questionTrees.length === 0) {
|
||||
throw Error(
|
||||
'The --quiz-- section should contain at least one question.'
|
||||
);
|
||||
}
|
||||
|
||||
questionTrees.forEach(singleQuestionNodes => {
|
||||
const questionTree = root(singleQuestionNodes);
|
||||
|
||||
const textNodes = getSection(questionTree, '--text--');
|
||||
const distractorNodes = getSection(questionTree, '--distractors--');
|
||||
const answerNodes = getSection(questionTree, '--answer--');
|
||||
const audioNodes = getSection(questionTree, '--audio--');
|
||||
|
||||
quizQuestions.push(
|
||||
getQuestion(textNodes, distractorNodes, answerNodes, audioNodes)
|
||||
);
|
||||
});
|
||||
|
||||
compiledQuizzes.push({ questions: quizQuestions });
|
||||
});
|
||||
|
||||
if (compiledQuizzes.length > 0) {
|
||||
file.data.quizzes = compiledQuizzes;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = plugin;
|
||||
Reference in New Issue
Block a user