const { test } = require('node:test'); const assert = require('node:assert'); const fs = require('node:fs'); const path = require('node:path'); const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8'); // The help pages are HTML too: the same structural walk validates them. const helpPages = ['help.html', 'help-ru.html'].map(f => ({ name: f, text: fs.readFileSync(path.join(__dirname, '..', f), 'utf8'), })); // The Russian dictionary must cover every user-visible key: the t('...') // literals in the script and every data-l10n / data-l10n-title attribute. // A missing key silently falls back to English - that is how the suite // views' Run/Move/Copy buttons stayed English until v3.22.5. test('every t() key and data-l10n attribute has a Russian entry', () => { const dictMatch = html.match(/const LANG_RU = \{([\s\S]*?)\n\};/); assert.ok(dictMatch, 'LANG_RU not found'); const norm = k => k .replace(/\\u([0-9a-fA-F]{4})/g, (_, h) => String.fromCharCode(parseInt(h, 16))) .replace(/\\'/g, "'") .replace(/"/g, '"').replace(/&/g, '&').replace(/'/g, "'"); const keys = new Set(); for (const m of dictMatch[1].matchAll(/^\s*(?:'((?:[^'\\]|\\.)*)'|"((?:[^"\\]|\\.)*)"):/gm)) { keys.add(norm(m[1] !== undefined ? m[1] : m[2])); } const missing = []; for (const m of html.matchAll(/\bt\(\s*'((?:[^'\\]|\\.)*)'\s*\)/g)) { if (!keys.has(norm(m[1]))) missing.push('t(): ' + m[1]); } for (const m of html.matchAll(/data-l10n(?:-title)?="([^"]+)"/g)) { if (!keys.has(norm(m[1]))) missing.push('data-l10n: ' + m[1]); } assert.deepStrictEqual(missing, [], 'missing Russian entries: ' + missing.join(' | ')); }); // Tags that never take an end tag. const VOID_TAGS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']); // End tags HTML allows to be omitted: a close tag may legally skip these. const OPTIONAL_END = new Set(['p', 'li', 'dt', 'dd', 'option', 'optgroup', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th', 'rt', 'rp', 'colgroup', 'caption']); // Start tags that implicitly close the listed open element (the subset used here). const IMPLIED_END = { li: ['li'], dt: ['dt', 'dd'], dd: ['dt', 'dd'], p: ['p'], option: ['option'], optgroup: ['optgroup'], tr: ['tr'], td: ['td', 'th'], th: ['td', 'th'], thead: ['thead', 'tbody', 'tfoot'], tbody: ['thead', 'tbody', 'tfoot'], tfoot: ['thead', 'tbody', 'tfoot'], }; // The markup without comments and embedded script/style bodies: only this // participates in tag nesting. JavaScript template strings may hold // unbalanced