搜题

在线答题搜答案脚本

"use strict";
/* eslint-disable no-underscore-dangle, @typescript-eslint/no-empty-function */
// ==UserScript==
// @name         搜题
// @namespace    search-answer
// @version      1.4
// @description  在线答题搜答案脚本
// @author       HCLonely
// @include      *
// @run-at       document-start
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @homepage     https://github.com/HCLonely/search-answer
// @require      https://cdn.jsdelivr.net/npm/sweetalert2@11
// @require      https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.slim.min.js
// @require      https://greasyfork.org/scripts/418102-tm-request/code/TM_request.js?version=902218
// @require      https://cdn.jsdelivr.net/npm/mammoth@1.4.21/mammoth.browser.min.js
// @require      https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js
// @require      https://cdn.jsdelivr.net/npm/tinykeys@1.4.0/dist/tinykeys.umd.min.js
// @require      https://cdn.jsdelivr.net/npm/tesseract.js@2.1.5/dist/tesseract.min.js
// @require      https://cdn.jsdelivr.net/npm/js-md5@0.7.3/build/md5.min.js
// @license      Apache-2.0
// @connect      www.baidu.com
// @connect      www.sogou.com
// @connect      cn.bing.com
// @connect      www.google.com
// ==/UserScript==
(() => {
    window.onblur = () => { };
    window.onfocus = () => { };
    document.onfocusin = () => { };
    document.onfocusout = () => { };
    document._addEventListener = document.addEventListener;
    document.addEventListener = (...argv) => {
        if (['visibilitychange', 'mozvisibilitychange', 'webkitvisibilitychange', 'msvisibilitychange'].includes(argv[0])) {
            return;
        }
        document._addEventListener(...argv);
    };
    document._removeEventListener = document.removeEventListener;
    document.removeEventListener = (...argv) => {
        if (['visibilitychange', 'mozvisibilitychange', 'webkitvisibilitychange', 'msvisibilitychange'].includes(argv[0])) {
            return;
        }
        document._removeEventListener(...argv);
    };
    window.onload = () => {
        window.onblur = () => { };
        window.onfocus = () => { };
        document.onfocusin = () => { };
        document.onfocusout = () => { };
    };
    let { highLightAbswer, startShortcutKey, ocrShortcutKey } = GM_getValue('settings') || {};
    const start = async () => {
        let data;
        let imageData;
        let engine = 'baidu';
        const searchFromWebPage = (text, engine) => {
            switch (engine) {
                case 'baidu':
                    window.open(`https://www.baidu.com/s?wd=${text}`, 'SearchResult', 'resize=yes,scrollbars=yes');
                    break;
                case 'sougou':
                    window.open(`https://www.sogou.com/web?query=${text}`, 'SearchResult', 'resize=yes,scrollbars=yes');
                    break;
                case 'bing':
                    window.open(`https://cn.bing.com/search?q=${text}`, 'SearchResult', 'resize=yes,scrollbars=yes');
                    break;
                case 'google':
                    window.open(`https://www.google.com/search?q=111${text}`, 'SearchResult', 'resize=yes,scrollbars=yes');
                    break;
                default:
                    window.open(`https://www.baidu.com/s?wd=${text}`, 'SearchResult', 'resize=yes,scrollbars=yes');
                    break;
            }
            return null;
        };
        const locate = (text, i = 0) => {
            const local = data.indexOf(text, i);
            if (local > -1) {
                return [local, ...locate(text, local + 1)];
            }
            return [];
        };
        const search = async (text) => {
            if (data === 'none') {
                return searchFromWebPage(text, engine);
            }
            const result = [];
            const local = locate(text);
            const regText = new RegExp(text, 'g');
            for (const i of local) {
                const matchResult = data.slice(i - 100, i + 500).replace(regText, `<font style="color:red">${text}</font>`);
                if (highLightAbswer) {
                    const arr = matchResult.split(text);
                    arr[1] = arr[1].replace(/[\w]+/, '<font style="color:red">$&</font>');
                    result.push(arr.join(text));
                    continue;
                }
                result.push(matchResult);
            }
            return result.filter((e) => e.trim()).map((e) => {
                if (!(e.includes('<img') && imageData && Object.keys(imageData).length > 0)) {
                    return e;
                }
                // eslint-disable-next-line
                Object.keys(imageData).map((imageMd5) => e.includes(`$${imageMd5}$`) && (e = e.replace(`$${imageMd5}$`, imageData[imageMd5])));
                return e;
            })
                .join('<br><hr data-content="分隔线">');
        };
        const readData = async () => {
            try {
                const imagesData = {};
                const data = await new Promise((res) => {
                    // eslint-disable-next-line max-len
                    const input = $('<input type="file" id="search-answer-js" style="width:50%;height:50%;color:red;position:fixed;left:25%;top:25%;background-color:red;z-index:99999999" title="点此加载题库" multiple="multiple">');
                    $('body').append(input);
                    input[0].addEventListener('change', async function selectedFileChanged() {
                        if (this.files?.length) {
                            Swal.fire('读取&处理中...', 'Excel格式文件和题目较多时处理较慢,请耐心等待!');
                            Swal.showLoading();
                            await new Promise((resolve) => {
                                setTimeout(() => {
                                    resolve(true);
                                }, 1000);
                            });
                            const text = (await Promise.all([...(this.files || [])].map((file) => new Promise((resolve) => {
                                const reader = new FileReader();
                                const fileName = file.name;
                                reader.onabort = () => resolve('');
                                reader.onerror = () => resolve('');
                                if (/.*?\.docx?$/.test(fileName)) {
                                    reader.onload = async () => {
                                        const arrayBuffer = reader.result;
                                        const options = {
                                            convertImage: mammoth.images.imgElement((image) => image.read('base64').then((imageBuffer) => {
                                                const imageMd5 = md5(imageBuffer);
                                                imagesData[imageMd5] = `data:${image.contentType};base64,${imageBuffer}`;
                                                return {
                                                    src: `$${imageMd5}$`
                                                };
                                            }))
                                        };
                                        const { value: fileData } = await mammoth.convertToHtml({ arrayBuffer }, options);
                                        resolve(fileData);
                                    };
                                    reader.readAsArrayBuffer(file);
                                }
                                else if (/.*?\.xlsx?$/.test(fileName)) {
                                    reader.onload = async () => {
                                        const arrayBuffer = reader.result;
                                        const { Sheets } = XLSX.read(arrayBuffer);
                                        // eslint-disable-next-line max-len
                                        const fileData = Object.values(Sheets).map((sheet) => XLSX.utils.sheet_to_json(sheet, { header: 1 }).map((cell) => cell.map((value) => value?.toString()?.trim()).filter((value) => value)
                                            .join(' | '))
                                            .join('<br/>'))
                                            .join('<br/>');
                                        resolve(fileData);
                                    };
                                    reader.readAsArrayBuffer(file);
                                }
                                else {
                                    reader.onload = () => {
                                        const fileData = reader.result;
                                        if (!fileData) {
                                            return resolve('');
                                        }
                                        resolve(fileData);
                                    };
                                    reader.readAsText(file);
                                }
                            })))).join('<br/>');
                            GM_setValue('data0', text);
                            GM_setValue('data1', imagesData);
                            input.remove();
                            Swal.fire('题库加载完毕!');
                            res(text);
                        }
                    });
                    document.querySelector('#search-answer-js').click();
                });
                return { text: data, image: imagesData };
            }
            catch (error) {
                console.error(error);
                Swal.fire('题库加载失败!', '详情请查看控制台', 'error');
                return {};
            }
        };
        await Swal.fire({
            title: '是否加载题库?',
            html: '加载题库:如果你有题库,请加载你的题库(推荐)<br/>直接运行:如之前加载过题库,并且不需要重新加载题库<br/>无题库模式:弹出网页显示搜索结果',
            confirmButtonText: '加载题库',
            showCancelButton: true,
            cancelButtonText: '直接运行',
            showDenyButton: true,
            denyButtonText: '无题库模式'
        }).then(async ({ isConfirmed, isDenied }) => {
            if (isConfirmed) {
                data = (await readData()).text;
                imageData = (await readData()).image;
            }
            else if (isDenied) {
                data = 'none';
                const { value: selectedEngine } = await Swal.fire({
                    title: '请选择搜索引擎',
                    input: 'radio',
                    inputOptions: {
                        baidu: '百度',
                        sougou: '搜狗',
                        bing: '必应',
                        google: '谷歌'
                    },
                    inputValidator: (value) => {
                        if (!value) {
                            return '请选择一个搜索引擎!';
                        }
                        return '';
                    }
                });
                if (selectedEngine) {
                    engine = selectedEngine;
                }
            }
            else {
                data = GM_getValue('data0');
                imageData = GM_getValue('data1');
            }
        });
        if (!data)
            return Swal.fire('加载题库失败', '', 'error');
        const icon = document.createElement('div');
        icon.innerHTML = '搜';
        icon.setAttribute('style', '' +
            'width:32px!important;' +
            'height:32px!important;' +
            'display:none!important;' +
            'background:#fff!important;' +
            'border-radius:16px!important;' +
            'box-shadow:4px 4px 8px #888!important;' +
            'position:absolute!important;' +
            'z-index:2147483647!important;' +
            'font-size: 24px;text-align-last: center;' +
            'cursor: pointer;' +
            '');
        icon.setAttribute('title', '搜索');
        document.documentElement.appendChild(icon);
        document.addEventListener('mousedown', (e) => {
            if (e.target === icon || (e.target?.parentNode === icon) || (e.target?.parentNode?.parentNode === icon)) {
                e.preventDefault();
            }
        });
        document.addEventListener('selectionchange', () => {
            if (!window.getSelection()?.toString()
                ?.trim()) {
                icon.style.display = 'none';
            }
        });
        document.addEventListener('mouseup', (e) => {
            if (e.target === icon || (e.target?.parentNode === icon) || (e.target?.parentNode?.parentNode === icon)) {
                e.preventDefault();
                return;
            }
            const text = window.getSelection()?.toString()
                ?.trim();
            if (text && icon.style.display === 'none') {
                icon.style.top = `${e.pageY + 12}px`;
                icon.style.left = `${e.pageX - 18}px`;
                icon.innerHTML = '搜';
                icon.setAttribute('title', '搜索');
                icon.style.display = 'block';
            }
            else if (!text) {
                icon.style.display = 'none';
            }
        });
        icon.addEventListener('click', async () => {
            const text = window.getSelection()?.toString()
                ?.trim();
            if (text) {
                icon.style.display = 'none';
                const result = await search(text);
                if (data && data !== 'none' && result !== null) {
                    Swal.fire({
                        html: result
                    });
                }
            }
        });
    };
    const settings = () => {
        Swal.fire({
            title: '设置',
            // eslint-disable-next-line max-len
            html: `<div class="setting"><input id="high-light-answer" type="checkbox"${highLightAbswer ? ' checked="checked"' : ''}/>高亮答案(仅支持题库模式且题目后面要紧跟"ABCD..."格式的答案)<br/>启动快捷键:<input id="start-shortcut-key" type="text" readonly="readonly" value="${startShortcutKey || ''}"/><br/>启动快捷键:<input id="ocr-shortcut-key" type="text" readonly="readonly" value="${ocrShortcutKey || ''}"/></div>`,
            preConfirm: () => ({
                highLightAbswer: $('#high-light-answer').is(':checked'),
                startShortcutKey: $('#start-shortcut-key').val(),
                ocrShortcutKey: $('#ocr-shortcut-key').val()
            })
        }).then(({ value }) => {
            highLightAbswer = value?.highLightAbswer;
            startShortcutKey = value?.startShortcutKey;
            ocrShortcutKey = value?.ocrShortcutKey;
            GM_setValue('settings', {
                highLightAbswer, startShortcutKey, ocrShortcutKey
            });
        });
        $('#start-shortcut-key,#ocr-shortcut-key').on('keydown', function (event) {
            let functionKey = '';
            if (event.metaKey) {
                functionKey += 'Meta+';
            }
            if (event.ctrlKey) {
                functionKey += 'Control+';
            }
            if (event.altKey) {
                functionKey += 'Alt+';
            }
            if (event.shiftKey) {
                functionKey += 'Shift+';
            }
            const keyValue = event.key.toUpperCase();
            $(this).val(functionKey + (['MEAT', 'ALT', 'CONTROL', 'SHIFT'].includes(keyValue) ? '' : keyValue));
        });
    };
    const OCR = async () => {
        const worker = Tesseract.createWorker({
            logger: (message) => console.log(message)
        });
        await worker.load();
        await worker.loadLanguage('eng+chi_sim+chi_tra');
        await worker.initialize('chi_sim');
        Swal.fire('正在进行OCR识别,请耐心等待...');
        Swal.showLoading();
        for (const element of $.makeArray($('img[src]:not(".ocred")'))) {
            try {
                const { data: { text } } = await worker.recognize(element);
                if (text) {
                    $(element).after(`<div>${text}</div>`);
                }
            }
            catch (e) {
                console.error(e);
            }
            $(element).addClass('ocred');
        }
        /*
        for (const element of $.makeArray($('body *:not(".ocred")')).filter((e) => /^url\(.*?\)$/.test($(e).css('backgroundImage')))) {
          const { data: { text } } = await worker.recognize($(element).css('backgroundImage')
            .replace('url("', '')
            .replace('")', ''));
          if (text) {
            $(element).after(`<div>${text}</div>`);
          }
          $(element).addClass('ocred');
        }
        */
        await worker.terminate();
        Swal.hideLoading();
        Swal.fire('OCR识别完成!', '', 'success');
    };
    const tinykeysOptions = {};
    if (startShortcutKey) {
        tinykeysOptions[startShortcutKey] = start;
    }
    if (ocrShortcutKey) {
        tinykeysOptions[ocrShortcutKey] = OCR;
    }
    window.tinykeys.default(window, tinykeysOptions);
    GM_registerMenuCommand('启动', start);
    GM_registerMenuCommand('设置', settings);
    GM_addStyle(`
.swal2-container {
  z-index: 9999999999 !important;
}
.swal2-html-container *{
  left:0;
  padding-left:0 !important;
  margin-left:0;
  border-left:0;
  width:100%;
}
.swal2-html-container hr{
  color: #a2a9b6;
  border: 0;
  font-size: 12px;
  padding: 1em 0;
  position: relative;
}
.swal2-html-container hr::before {
  content: attr(data-content);
  position: absolute;
  padding: 0 1ch;
  line-height: 1px;
  border: solid #d0d0d5;
  border-width: 0 99vw;
  width: fit-content;
  white-space: nowrap;
  left: 50%;
  transform: translateX(-50%);
}
.swal2-html-container hr::after{
  content: attr(data-content);
  position: absolute;
  padding: 4px 1ch;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  color: transparent;
  border: 1px solid #d0d0d5;
}
.swal2-html-container .setting {
  text-align: left;
}
.swal2-html-container input[type="checkbox"]{
  width: 15px;
}
.swal2-html-container input[type="text"]{
  width: 200px;
  border: 2px solid #00a9fd;
  border-radius: 5px;
  font-size: 15px;
}
`);
})();