The Course pane lists the 21 lessons, shows a lesson's text and exercise, loads its starter, and checks the program in the editor with the engine (engine_course_check in the page, the same commands in the desktop app): each case's verdict, each rule, What went wrong? from the diagnostics, the verdict and the critique, the hints one at a time with the engine's pointer, and a hint from the assistant when a model is connected, any program in its answer held back. Progress is kept only in this browser and says so. The interface's words are data, locales/<locale>/<area>.json for every part, English the source and Simplified Chinese marked machine-assisted; the Studio follows the browser's language until the reader picks one with the switch. test/locales.test.mjs holds every locale to every key and placeholder. The e2e checks the course in English and in Chinese. wai-web's engine tests pin 23 native and 16 wasm32, a course check the same bytes in both. Co-Authored-By: Claude Opus 5.5 <[email protected]>
54 lines
2.2 KiB
JavaScript
54 lines
2.2 KiB
JavaScript
// A lesson's Markdown as nodes, never as HTML: paragraphs, headings, lists,
|
|
// fenced code (its info string's first word as the language), inline code,
|
|
// **bold** and *emphasis*. HTML comments, such as a translation's note, are
|
|
// left out.
|
|
import { el } from './dom.js';
|
|
|
|
// **bold**, *emphasis* and `code` within a line.
|
|
export function inline(text) {
|
|
return text.split(/(\*\*[^*]+\*\*|`[^`]+`|\*[^*\s][^*]*\*)/).filter(Boolean).map((p) => {
|
|
if (p.startsWith('**') && p.endsWith('**')) return el('strong', { text: p.slice(2, -2) });
|
|
if (p.startsWith('`') && p.endsWith('`')) return el('code', { text: p.slice(1, -1) });
|
|
if (p.length > 2 && p.startsWith('*') && p.endsWith('*')) return el('em', { text: p.slice(1, -1) });
|
|
return document.createTextNode(p);
|
|
});
|
|
}
|
|
|
|
export function markdown(src) {
|
|
const out = [];
|
|
const lines = String(src || '').replace(/<!--[\s\S]*?-->/g, '').split('\n');
|
|
let para = [];
|
|
let list = null;
|
|
const flush = () => {
|
|
if (para.length) out.push(el('p', {}, ...inline(para.join(' '))));
|
|
para = [];
|
|
};
|
|
const endList = () => { if (list) out.push(list); list = null; };
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
const fence = line.match(/^```(\S*)/);
|
|
if (fence) {
|
|
flush(); endList();
|
|
const body = [];
|
|
for (i++; i < lines.length && !lines[i].startsWith('```'); i++) body.push(lines[i]);
|
|
out.push(el('pre', { class: 'qs-code', 'data-lang': fence[1] || 'text' }, el('code', { text: body.join('\n') })));
|
|
continue;
|
|
}
|
|
const h = line.match(/^(#{1,4})\s+(.*)$/);
|
|
if (h) { flush(); endList(); out.push(el(`h${Math.min(6, h[1].length + 2)}`, {}, ...inline(h[2]))); continue; }
|
|
const item = line.match(/^\s*(?:[-*]|\d+\.)\s+(.*)$/);
|
|
if (item) {
|
|
flush();
|
|
const ordered = /^\s*\d+\./.test(line);
|
|
if (!list || (list.tagName === 'OL') !== ordered) { endList(); list = el(ordered ? 'ol' : 'ul'); }
|
|
list.append(el('li', {}, ...inline(item[1])));
|
|
continue;
|
|
}
|
|
if (!line.trim()) { flush(); endList(); continue; }
|
|
if (list && /^\s{2,}\S/.test(line)) { list.lastChild.append(' ', ...inline(line.trim())); continue; }
|
|
endList();
|
|
para.push(line.trim());
|
|
}
|
|
flush(); endList();
|
|
return out;
|
|
}
|