// ==UserScript==
// @name 科举小抄
// @namespace https://github.com/fuckKeju/fuckKeju
// @version 0.0.5
// @description 科举小抄 - 阿里云大学“科考”辅助工具
// @author fuckKeju
// @match *.developer.aliyun.com/*
// @run-at document-start
// @grant unsafeWindow
// ==/UserScript==
/* 题库数据 */
var customQuestionsDatabase = []
var useCustomQuestionsDatabase = false
async function getPageWindow () {
return new Promise(function (resolve, reject) {
if (window._pageWindow) {
return resolve(window._pageWindow)
}
const listenEventList = ['load', 'mousemove', 'scroll', 'get-page-window-event']
function getWin (event) {
window._pageWindow = this
// debug.log('getPageWindow succeed', event)
listenEventList.forEach(eventType => {
window.removeEventListener(eventType, getWin, true)
})
resolve(window._pageWindow)
}
listenEventList.forEach(eventType => {
window.addEventListener(eventType, getWin, true)
})
/* 自行派发事件以便用最短的时候获得pageWindow对象 */
window.dispatchEvent(new window.Event('get-page-window-event'))
})
}
getPageWindow()
/* 修正标题字符串 */
function trimTitle (title, removeSerialNumber) {
title = title || ''
title = title.replace(/\s+/gi, ' ').replace(/\?{2,}/gi, ' ')
if (removeSerialNumber) {
title = title.replace(/^\d+\./, '')
}
return title
}
/* 提取答案字符串 */
function fixAnswer (answer) {
answer = answer || ''
return answer.replace(/^[A-Za-z]\.\s/, '').replace(/\s+--checked/, '')
}
/**
* 判断两条题目是否为同一条题目
* @param questionA
* @param questionB
*/
function isTheSameQuestion (questionA, questionB) {
let isSame = true
const titleA = trimTitle(questionA.title, true)
const titleB = trimTitle(questionB.title, true)
if (titleA === titleB) {
for (let i = 0; i < questionA.answerList.length; i++) {
const answerA = fixAnswer(questionA.answerList[i])
let hasSameAnswer = false
for (let j = 0; j < questionB.answerList.length; j++) {
const answerB = fixAnswer(questionB.answerList[j])
if (answerA === answerB) {
hasSameAnswer = true
break
}
}
if (!hasSameAnswer) {
isSame = false
break
}
}
} else {
isSame = false
}
// isSame && console.log(titleA, titleB, isSame)
return isSame
}
/* 因为收集了部分异常数据,为了排查异常数据的干扰,所以需要进行是否异常的判断 */
function isNormalQuestion (question) {
return /\s+--checked/.test(JSON.stringify(question.answerList))
}
function eachQuestionsDatabase (questionsDatabase, callback) {
questionsDatabase.forEach((items, index) => {
if (Array.isArray(items)) {
items.forEach(curQuestion => {
callback(curQuestion, index)
})
} else {
callback(items, index)
}
})
}
function getQuestionsDatabase () {
const subjectEl = document.querySelector('.yq-main-examination .top-info h2.title-content')
let questionsDatabase = []
try {
if (subjectEl) {
questionsDatabase = JSON.parse(localStorage.getItem(subjectEl.innerText) || '[]')
} else {
questionsDatabase = customQuestionsDatabase
}
} catch (e) {
questionsDatabase = []
}
return questionsDatabase
}
/* 从混乱的题库集里提取整理后的题库 */
function extractQuestionList (questionsDatabase) {
const questionList = []
let addCount = 0
function addToQuestionList (question) {
addCount++
// console.log(question, addCount)
if (!question || !question.title || !Array.isArray(question.answerList)) {
return false
}
let hasSameQuestion = false
for (let i = 0; i < questionList.length; i++) {
const questionB = questionList[i]
if (isTheSameQuestion(question, questionB)) {
hasSameQuestion = true
if (isNormalQuestion(question) && question.rightAnswer === '答案正确') {
questionList[i] = question
} else {
questionList[i].relatedQuestions = questionList[i].relatedQuestions || []
questionList[i].relatedQuestions.push(question)
}
break
}
}
if (!hasSameQuestion) {
questionList.push(question)
}
}
eachQuestionsDatabase(questionsDatabase, (question, index) => {
addToQuestionList(question, index)
})
return questionList
}
// console.log(extractQuestionList(customQuestionsDatabase))
/**
* 从某个题库数据集里查找是否存在相关的题目
* @param questionsDatabase
* @param questions
*/
function searchRelatedQuestions (questionsDatabase, questions) {
let relatedQuestions = []
eachQuestionsDatabase(questionsDatabase, (questionsA) => {
if (isTheSameQuestion(questionsA, questions)) {
relatedQuestions.push(questionsA)
}
})
/* 查找是否存在答对的历史记录,优先显示答对的数据 */
if (relatedQuestions.length > 1) {
const rightAnswerArr = []
const wrongAnswerArr = []
relatedQuestions.forEach(question => {
if (question.rightAnswer === '答案正确' && isNormalQuestion(question)) {
rightAnswerArr.push(question)
} else {
wrongAnswerArr.push(question)
}
})
relatedQuestions = rightAnswerArr.concat(wrongAnswerArr)
}
return relatedQuestions
}
/**
* 判断某条题目的相关问答库里是否包含一样的答案记录
* @param questions
* @param relatedQuestions
*/
function hasTheSameQuestionsInRelatedQuestions (questions, relatedQuestions) {
let hasSame = false
relatedQuestions = relatedQuestions || []
for (let i = 0; i < relatedQuestions.length; i++) {
const relatedQuestion = relatedQuestions[i]
let isSame = true
for (let j = 0; j < relatedQuestion.answerList.length; j++) {
const answer = relatedQuestion.answerList[j]
const relatedQuestionChecked = /\s+--checked/.test(answer)
const questionsChecked = /\s+--checked/.test(questions.answerList[j])
if (relatedQuestionChecked !== questionsChecked) {
isSame = false
break
}
}
if (isSame) {
hasSame = true
break
}
}
return hasSame
}
/**
* 遍历页面上的题目并进行回调,该方法必须在试题页面才能运行
* @param callback
* @returns {[]}
*/
function eachQuestionItem (callback) {
const result = []
const isExamMode = document.querySelector('.yq-main-examination .time-info')
const items = document.querySelectorAll('.question-panel .question-item')
if (items) {
items.forEach(questionItemEl => {
const type = questionItemEl.querySelector('.q-title .q-tag').innerText.trim()
const title = trimTitle(questionItemEl.querySelector('.q-title .q-t-text').innerText.trim())
const answerList = []
const answerListEl = questionItemEl.querySelectorAll('.q-option .answer-text')
answerListEl.forEach(answerEl => {
let answer = answerEl.innerText.trim()
const checkedEl = answerEl.parentNode.querySelector('input')
if (checkedEl && checkedEl.checked) {
answer += ' --checked'
}
answerList.push(answer)
})
const questionObj = {
title,
type,
answerList
}
const pointEl = questionItemEl.querySelector('.e-point .p-detail')
if (pointEl) {
questionObj.point = '相关知识点:' + pointEl.innerText.trim()
} else {
questionObj.point = '未匹配到任何相关知识点'
}
const rightAnswerEl = questionItemEl.querySelector('.right-answer')
if (rightAnswerEl) {
questionObj.rightAnswer = rightAnswerEl.innerText.trim() || '答案正确'
} else {
if (isExamMode) {
questionObj.rightAnswer = '答案未知'
} else {
questionObj.rightAnswer = '答案正确'
}
}
result.push(questionObj)
if (callback instanceof Function) {
try {
callback(questionObj, questionItemEl)
} catch (err) {
console.error('eachQuestionItem error:', err, questionObj, questionItemEl)
}
}
})
}
return result
}
/* 添加相关题目内容到题目面板下面,并且添加显示隐藏事件 */
function addRelatedQuestionsDom (questionItemEl, relatedQuestions) {
const dd = document.createElement('dd')
dd.setAttribute('class', 'relatedQuestions')
dd.style.marginTop = '30px'
dd.style.display = 'none'
dd.style.border = '1px solid #ccc'
dd.style.borderRadius = '5px'
// dd.style.padding = '10px'
// dd.style.backgroundColor = '#f9f9f9'
if (questionItemEl.querySelector('.relatedQuestions')) {
questionItemEl.removeChild(questionItemEl.querySelector('.relatedQuestions'))
}
if (relatedQuestions.length) {
const codeEl = document.createElement('pre')
codeEl.style.border = 'none'
codeEl.innerHTML = JSON.stringify(relatedQuestions, null, 2)
dd.appendChild(codeEl)
questionItemEl.appendChild(dd)
} else {
dd.innerText = '暂无相关题目信息,先考几遍,然后查看考试结果再试试吧'
questionItemEl.appendChild(dd)
}
questionItemEl.ondblclick = function (event) {
const relatedQuestions = questionItemEl.querySelector('.relatedQuestions')
if (relatedQuestions) {
if (relatedQuestions.style.display === 'none') {
relatedQuestions.style.display = 'block'
relatedQuestions.style.opacity = 0.4
relatedQuestions.style.overflow = 'auto'
relatedQuestions.style.maxHeight = '200px'
} else {
relatedQuestions.style.display = 'none'
}
}
}
}
/**
* 自动匹配题目并尝试自动填充对应答案
* @param questionsDatabase
*/
function autoMatchQuestionAndCheckedAnswer (questionsDatabase) {
eachQuestionItem((questions, questionItemEl) => {
const relatedQuestions = searchRelatedQuestions(questionsDatabase, questions)
if (relatedQuestions.length) {
const relatedQuestion = relatedQuestions[0]
if (isNormalQuestion(relatedQuestion) && relatedQuestion.rightAnswer === '答案正确') {
relatedQuestion.answerList.forEach((answer, index) => {
if (/\s+--checked/.test(answer)) {
const answerLabel = questionItemEl.querySelectorAll('label.option-label')
if (answerLabel[index]) {
answerLabel[index].click()
}
}
})
}
} else {
console.log('以下题目无法匹配答案:', questions, questionItemEl, relatedQuestions)
}
})
}
/* 隐藏相关题目面板 */
function hideRelatedQuestions () {
const relatedQuestionsEls = document.querySelectorAll('.relatedQuestions')
relatedQuestionsEls.forEach(item => {
item.style.display = 'none'
})
}
let hasInit = false
async function fuckKeju () {
if (hasInit) { return false }
console.log('科举小抄 init suc')
hasInit = true
const subjectTitle = document.querySelector('.yq-main-examination .top-info h2.title-content').innerText
const isExamMode = document.querySelector('.yq-main-examination .time-info')
let questionsDatabase = getQuestionsDatabase()
/* 使用预置数据,而非定义的数据 */
if (useCustomQuestionsDatabase) {
questionsDatabase = customQuestionsDatabase
}
let findNewQuestion = false
const curQuestionsList = eachQuestionItem((questions, questionItemEl) => {
const relatedQuestions = searchRelatedQuestions(questionsDatabase, questions)
addRelatedQuestionsDom(questionItemEl, relatedQuestions)
/* 收集新题目数据 */
if (!isExamMode && !hasTheSameQuestionsInRelatedQuestions(questions, relatedQuestions)) {
findNewQuestion = true
questionsDatabase.push(questions)
if (findNewQuestion) {
console.log('发现新的题目,或新的答案记录:', questions)
}
}
})
/* 提示到控制面板,用于手动收集题目数据 */
console.log(JSON.stringify(curQuestionsList, null, 2))
/* 重新写入收集到的题目数据 */
if (findNewQuestion) {
// localStorage.setItem(subjectTitle, JSON.stringify(questionsDatabase))
}
localStorage.setItem(subjectTitle, JSON.stringify(questionsDatabase))
/* 考试模式下双击标题尝试自填充答案 */
const subjectEl = document.querySelector('.yq-main-examination .top-info h2.title-content')
subjectEl.ondblclick = function () {
if (isExamMode) {
autoMatchQuestionAndCheckedAnswer(questionsDatabase)
}
}
/* 切换题目时候,隐藏小抄 */
const switchDoms = document.querySelectorAll('.question-num span.item')
const switchDoms02 = document.querySelectorAll('.e-opt-panel a')
switchDoms.forEach(el => {
el.onmouseenter = hideRelatedQuestions
})
switchDoms02.forEach(el => {
el.onclick = hideRelatedQuestions
})
/* 通过控制面板提取题库 */
const pageWindow = await getPageWindow()
pageWindow.extractQuestionList = function (print) {
const questionsDatabase = getQuestionsDatabase()
const questionList = extractQuestionList(questionsDatabase)
if (print) {
console.log(JSON.stringify(questionList, null, 2))
}
return questionList
}
}
function ready (selector, fn, shadowRoot) {
const listeners = []
const win = window
const doc = shadowRoot || win.document
const MutationObserver = win.MutationObserver || win.WebKitMutationObserver
let observer
function $ready (selector, fn) {
// 储存选择器和回调函数
listeners.push({
selector: selector,
fn: fn
})
if (!observer) {
// 监听document变化
observer = new MutationObserver(check)
observer.observe(shadowRoot || doc.documentElement, {
childList: true,
subtree: true
})
}
// 检查该节点是否已经在DOM中
check()
}
function check () {
for (let i = 0; i < listeners.length; i++) {
var listener = listeners[i]
var elements = doc.querySelectorAll(listener.selector)
for (let j = 0; j < elements.length; j++) {
var element = elements[j]
if (!element._isMutationReady_) {
element._isMutationReady_ = true
listener.fn.call(element, element)
}
}
}
}
$ready(selector, fn)
}
ready('.question-panel .question-item', () => {
/**
* 此处必须延迟执行,题目渲染和选中渲染是异步操作
* 需要延时等待选中的渲染成功才执行初始化逻辑
*/
console.log('检查到进入了试题页面,即将为你初始化小抄逻辑')
setTimeout(function () {
fuckKeju()
}, 1000 * 3)
})