/* Visible attempt labels shared by course quizzes and course exams. */ (function (root) { 'use strict'; const IGNORE = [ 'script', 'style', 'noscript', 'template', 'textarea', 'input', 'select', 'option', 'pre', 'code', 'svg', 'canvas', '.examtest_content_question', '.examtest_content_list', '.examtest_content_list_des', '.pc_examtest_content_question', '.pc_examtest_content_list', '.model_answer_wrapper', '.model_answer', '.model_dec', '[data-question-stem]', '[data-question-text]', '[data-question-options]', '[data-answer-text]', '.question-stem', '.question-text', '.question-options', '.question-answer', '.option-text', '.answer-text', '[role="option"]' ].join(','); const BLOCK = /^(?:ADDRESS|ARTICLE|ASIDE|BLOCKQUOTE|DD|DIV|DL|DT|FIELDSET|FIGCAPTION|FIGURE|FOOTER|FORM|H[1-6]|HEADER|HR|LI|MAIN|NAV|OL|P|SECTION|TABLE|TBODY|TD|TFOOT|TH|THEAD|TR|UL)$/; const DIGITS = '[0-9零〇一二三四五六七八九十百两]+'; const TARGET = '(?:答题|作答|考试)'; const CAPTION = '(?:允许|允许的|可用|可|总|总共|最多|最大|剩余|还剩|还有|还可)'; const SEPARATOR = '(?:[::=]|为|是)?'; const LIMIT = '(?:仅|只有|仅有|只限)?'; const UNIT = '(?:次|[((]次[))])?'; const forms = [ new RegExp('^(?:本次|本场)?' + CAPTION + '(?:可|允许)?' + TARGET + '(?:总|剩余)?(?:次数|机会)' + SEPARATOR + LIMIT + '(' + DIGITS + ')' + UNIT + '$'), new RegExp('^(?:本次|本场)?' + TARGET + '(?:允许|可用|总|最多|最大|剩余|还剩)(?:次数|机会)' + SEPARATOR + LIMIT + '(' + DIGITS + ')' + UNIT + '$'), new RegExp('^(?:本次|本场)?' + TARGET + '(?:次数|机会)' + SEPARATOR + '(?:上限|最多|最大|仅剩|还剩|剩余)' + SEPARATOR + LIMIT + '(' + DIGITS + ')' + UNIT + '$'), new RegExp('^(?:本次|本场)?(?:仅|只)(?:有|可|能|允许)?' + TARGET + '(' + DIGITS + ')次$'), new RegExp('^(?:本次|本场)?(?:最多|总共|总|允许|还可|剩余)(?:可)?' + TARGET + '(' + DIGITS + ')次$'), new RegExp('^(?:本次|本场)?(?:仅|只)(?:有|剩)?(' + DIGITS + ')次' + TARGET + '机会$'), new RegExp('^(?:本次|本场)?(?:剩余|还剩|还有)(' + DIGITS + ')次' + TARGET + '机会$'), new RegExp('^(?:本次|本场)?还可(?:补考|重考)' + SEPARATOR + '(' + DIGITS + ')次$'), // The saved site's showButton2 label is “不及格,还可补考 N 次”. // The template may wrap it in parentheses after the normal retake button. new RegExp('^不及格[,,]还可补考(' + DIGITS + ')次$'), new RegExp('^(?:重新答题|重新考试)?[((]不及格[,,]还可补考(' + DIGITS + ')次[))]$') ]; function clean(value) { return String(value == null ? '' : value).replace(/[\u200b-\u200d\u2060\uFEFF]/g, '').replace(/[0-9]/g, char => String(char.charCodeAt(0) - 0xff10)).replace(/\s+/g, ' ').trim(); } function visible(node) { if (!node || !node.isConnected || node.closest('[hidden],[aria-hidden="true"],[inert]')) return false; const view = node.ownerDocument && node.ownerDocument.defaultView; if (!view) return false; for (let current = node; current && current.nodeType === 1; current = current.parentElement) { const style = view.getComputedStyle(current); if (style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse' || Number(style.opacity) === 0) return false; } return node.getClientRects().length > 0 || view.getComputedStyle(node).display === 'contents'; } function numberValue(raw) { if (/^[0-9]+$/.test(raw)) return Number(raw); if (/^[零〇]+$/.test(raw)) return 0; if (raw === '一') return 1; // Only zero and one affect this policy. Other explicit Chinese counts still // form part of the signature but are never mistaken for a single attempt. return 2; } function evidenceText(scope) { const candidates = new Set(); function walk(node) { if (node.nodeType === 3) return node.textContent; if (node.nodeType !== 1 || node.matches(IGNORE) || !visible(node)) return ''; if (node.tagName === 'BR') return '\n'; const text = Array.from(node.childNodes, walk).join(' '); // Small DOM rows permit labels and values in separate inline spans while // whole pages and unrelated paragraphs are never treated as one label. if (text.length <= 240) candidates.add(clean(text)); return BLOCK.test(node.tagName) ? '\n' + text + '\n' : text; } const lines = walk(scope).split(/[\n,,;;。!!]+/).map(clean).filter(Boolean); lines.forEach(line => { if (line.length <= 240) candidates.add(line); }); // Definition lists commonly render a status label and its value separately. scope.querySelectorAll('dt').forEach(label => { const value = label.nextElementSibling; if (value && value.tagName === 'DD' && visible(label) && visible(value) && !label.closest(IGNORE) && !value.closest(IGNORE)) { candidates.add(clean(walk(label) + ' ' + walk(value))); } }); return candidates; } function read(scope) { const empty = { blocked: false, known: false, count: null, reason: '', signature: '[]' }; // The caller must pass the current exam/quiz container. Reading Document or // body could borrow another course's limits or a hidden background dialog. if (!scope || scope.nodeType !== 1 || /^(?:HTML|BODY)$/.test(scope.tagName) || !visible(scope) || scope.closest(IGNORE)) return empty; const evidence = new Set(); evidenceText(scope).forEach(raw => { const value = raw.replace(/\s+/g, '').replace(/[::]$/, ''); for (const expression of forms) { const match = value.match(expression); if (!match) continue; evidence.add(JSON.stringify([value, numberValue(match[1])])); break; } }); const labels = Array.from(evidence).sort(); const values = labels.map(item => JSON.parse(item)[1]); const count = values.includes(0) ? 0 : values.includes(1) ? 1 : null; return { blocked: count !== null, known: labels.length > 0, count: count, reason: count === 0 ? '网页明确提示已无可用答题或考试次数,等待人工处理' : count === 1 ? '网页明确提示仅允许或剩余 1 次答题或考试机会,等待人工处理' : '', signature: JSON.stringify(labels.map(item => JSON.parse(item))) }; } const api = { read: read }; if (typeof module === 'object' && module.exports) module.exports = api; if (root) root.YianAttemptPolicy = api; })(typeof window === 'object' ? window : typeof globalThis === 'object' ? globalThis : this);