fix: resume video playback and advance linked exams with attempt controls
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
/* 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);
|
||||
Reference in New Issue
Block a user