ankienhance

anki extract sentence enhance

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greasyfork.org/scripts/534396/1601562/ankienhance.js

;const {ankiFetchClickFn, ankiFetchData, setAllBold, getAnkiFetchParams, arrayDiff} = (() => {
    PushHookAnkiStyle(`
    .fetch-sentence-container { display:flex; }
    .fetch-item:nth-child(2) button.fetch-delete,.fetch-hidden,.fetch-dd:has(option[value="html"]:checked) + .fetch-dd{ display: none}
    .fetch-opera { display: grid; justify-items: center}
    .fetch-item { margin-top: 1vw; margin-left: 1vw; border: 1px dashed #e9b985; padding:.4vw}
    .fetch-item-specific { border-color: #13195a}
    .fetch-box { 
            display: inline-block;
            vertical-align: middle;
            margin-left: 0.2vw;
        }
    .fetch-buttons {display: inline-block;}
    .fetch-buttons button {display: block;}
    .fetch-dd { margin-left: 0vw; }
    .fetch-name {width: 7vw;}
    .fetch-format {width: 20vw}
    .fetch-bold-field,.fetch-html-replacement,.fetch-value-replacement {width: 17vw}
    .fetch-num { width:3vw}
    .moving {
            background: transparent;
            color: transparent;
            border: 1px dashed #ccc;
        }
    `);

    PushHookAnkiDidRender(addOrDelBtn);

    function changeAddDelBtn(ev, fn) {
        fn(ev);
        addOrDelBtn();
    }

    PushHookAnkiChange('#model', changeAddDelBtn);
    PushHookAnkiChange('.field-name', changeAddDelBtn);

    PushExpandAnkiInputButton('hammer', '', changeAddDelBtn);

    PushExpandAnkiInputButton('fetch-all', '', () => {
        document.querySelectorAll('.fetch-sentence-field').forEach(button => button.click());
    });
    PushExpandAnkiInputButton('fetch-delete', '', (e) => {
        findParent(e.target, '.fetch-item').remove();
    });
    PushExpandAnkiInputButton('fetch-add', '', (e) => {
        findParent(e.target, '.fetch-item').insertAdjacentElement('afterend', buildFetchItem({...de}));
    });
    PushExpandAnkiInputButton('fetch-export', '', () => {
        const data = JSON.stringify(getAnkiFetchParams('', false));
        download('fetch-rule.json', data);
    });
    PushExpandAnkiInputButton('fetch-import', '', (ev) => {
        ev.target.parentElement.lastElementChild.click();
    });

    function diff(a, b, fn) {
        if (b.length <= 2 || (a.length <= b.length)) {
            return a.filter(aa => {
                let flag = false
                for (const bb of b) {
                    if (fn(aa, bb)) {
                        flag = true
                        break
                    }
                }
                return !flag;
            })
        }
        // maybe have some optimization
        const hadIndex = new Set();
        b.forEach(bb => {
            for (const i in a) {
                const aa = a[i];
                if (fn(aa, bb)) {
                    hadIndex.add(parseInt(i));
                    break
                }
            }
        });
        return a.filter((aa, i) => !hadIndex.has(i));
    }

    function objectsEqual(o1, o2) {
        return Object.keys(o1).length === Object.keys(o2).length
            && Object.keys(o1).every(p => o1[p] === o2[p])
    }

    PushHookAnkiChange('.fetch-file', async (ev) => {
        const file = ev.target.files[0];
        if (!file) {
            Swal.showValidationMessage('没有文件!');
            return
        }
        const items = await file.text().then(JSON.parse);
        if (!items || items.length < 1 || !items[0].hasOwnProperty('fetch-name') || !items[0].hasOwnProperty('fetch-to-field')) {
            Swal.showValidationMessage('不是正确的规则文件!');
            return
        }
        const hadRule = getAnkiFetchParams('', false);
        const newRule = diff(items, hadRule, objectsEqual);
        if (newRule.length < 1) {
            Swal.showValidationMessage('无需导入!');
            return
        }
        const names = [];
        newRule.forEach(item => {
            const t = buildFetchItem(item);
            t.classList.add('fetch-item-specific');
            setting.appendChild(t);
            names.push([item['fetch-name'], item['fetch-name']]);
        });
        Swal.showValidationMessage(`已导入${newRule.length}条记录!`);
        if (GM_getValue('fetch-display-type', 1) === 2) {
            const options = buildOption(names, '', 0, 1);
            setting.children[0].insertAdjacentHTML('beforeend', options);
        }
    });
    PushExpandAnkiInputButton('fetch-copy', '', (e) => {
        const item = findParent(e.target, '.fetch-item');
        const copyItem = item.cloneNode(true);
        copyItem.querySelector('.fetch-active').checked = false;
        item.insertAdjacentElement('afterend', copyItem);
    });

    PushExpandAnkiInputButton('fetch-sentence-field', '', (ev) => {
        ankiFetchClickFn(ev.target);
    }, '', (ev) => {
        const button = ev.target;
        const targetField = button.parentElement.parentElement.querySelector('.sentence_field,.field-name').value.trim();
        const targetEle = button.parentElement.parentElement.querySelector('.spell-content,.field-value');
        const arr = getAnkiFetchParams(targetField, false);
        if (!arr || arr.length < 1) {
            return
        }
        ev.preventDefault();
        const sel = document.createElement('select');
        const map = {};
        const opts = arr.map(v => {
            map[v['fetch-name']] = v;
            return [v['fetch-name'], v['fetch-name']];
        });
        opts.unshift(['', '选择一个操作']);
        sel.innerHTML = buildOption(opts, 0, 0, 1);
        const fn = (ev) => {
            if (sel.value) {
                ankiFetchData(map[sel.value], targetEle);
            }
            const evt = ev.type === 'click' ? 'change' : 'blur';
            sel.removeEventListener(evt, fn);
            sel.parentElement.replaceChild(button, sel)
        };
        sel.addEventListener('blur', fn);
        sel.addEventListener('change', fn);
        button.parentElement.replaceChild(sel, button);
    });

    PushHookAnkiDidRender(() => document.addEventListener('mousedown', fullBold));
    PushHookAnkiDidRender(() => document.addEventListener('mouseup', fullBold));
    PushHookAnkiClose(() => document.removeEventListener('mousedown', fullBold));
    PushHookAnkiClose(() => document.removeEventListener('mouseup', fullBold));

    PushHookAnkiDidRender(() => setting.addEventListener('dblclick', settingItemSwitchDisplay));
    PushHookAnkiClose(() => setting.removeEventListener('dblclick', settingItemSwitchDisplay));

    function download(filename, text, type = "text/plain") {
        // Create an invisible A element
        const a = document.createElement("a");
        a.style.display = "none";
        document.body.appendChild(a);

        // Set the HREF to a Blob representation of the data to be downloaded
        a.href = window.URL.createObjectURL(
            new Blob([text], {type})
        );

        // Use download attribute to set set desired file name
        a.setAttribute("download", filename);

        // Trigger the download by simulating click
        a.click();

        // Cleanup
        window.URL.revokeObjectURL(a.href);
        document.body.removeChild(a);
    }

    function settingItemSwitchDisplay(ev) {
        if (!ev.target.classList.contains('fetch-item')) {
            return
        }
        const sel = setting.children[0];
        const items = [];
        const displayType = GM_getValue('fetch-display-type', 1);
        setting.querySelectorAll('.fetch-item').forEach(item => {
            items.push(item.querySelector('.fetch-name').value);
            if (displayType === 1 && item !== ev.target) {
                item.classList.add('fetch-hidden');
                return
            }
            item.classList.remove('fetch-hidden');
        });
        if (displayType === 2) {
            sel.classList.add('fetch-hidden');
            ev.target.scrollIntoView();
            ev.target.classList.add('fetch-item-specific');
            GM_setValue('fetch-display-type', 1);
            return;
        }
        const opts = items.map(name => [name, name])
        sel.innerHTML = buildOption(opts, ev.target.querySelector('.fetch-name').value, 0, 1)
        sel.classList.remove('fetch-hidden');
        GM_setValue('fetch-display-type', 2);
    }

    PushHookAnkiChange('.fetch-item-select', (ev) => {
        const name = ev.target.value;
        const t = setting.querySelector(`.fetch-name[value='${name}']`);
        if (!t) {
            return
        }
        setting.querySelectorAll('.fetch-item:not(.fetch-hidden)').forEach(e => e.classList.add('fetch-hidden'));
        findParent(t, '.fetch-item').classList.remove('fetch-hidden');
    });

    PushHookAnkiChange('#fetch.swal2-checkbox', (ev) => {
        if (!ev.target.checked) {
            saveFetchItems();
            addOrDelBtn();
            setting.children[0].classList.add('fetch-hidden');
            getFetchItemEles().map(e => e.remove());
            ev.target.parentElement.querySelectorAll('.fetch-import,.fetch-export').forEach(btn => btn.classList.add('fetch-hidden'))
            return
        }
        let fetchItems = GM_getValue('fetch-items', [{...de}]);
        fetchItems = fetchItems.length > 0 ? fetchItems : [{...de}];
        fetchItems.forEach(item => setting.appendChild(buildFetchItem(item)));
        if (GM_getValue('fetch-display-type', 1) === 2) {
            setting.children[0].innerHTML = buildOption(fetchItems.map(m => [m['fetch-name'], m['fetch-name']]), '', 0, 1);
            setting.children[0].classList.remove('fetch-hidden');
            setting.children.length > 2 && [...setting.children].slice(2).forEach(e => e.classList.add('fetch-hidden'));
        }
        ev.target.parentElement.querySelectorAll('.fetch-import,.fetch-export').forEach(btn => btn.classList.remove('fetch-hidden'))
    });

    PushHookAnkiChange('.fetch-active', fetchActive);

    ['swal2-cancel swal2-styled',
        'swal2-confirm swal2-styled',
        'swal2-container swal2-center swal2-backdrop-hide'].forEach(className => {
        PushExpandAnkiInputButton(className, '', saveFetchItems);
    });

    const fetchFields = ['fetch-name', 'fetch-field', 'fetch-to-field', 'fetch-selector', 'fetch-parent-selector', 'fetch-data-type',
        'fetch-exclude-selector', 'fetch-join-selector', 'fetch-format', 'fetch-data-handle', 'fetch-repeat',
        'fetch-bold-field', 'fetch-num', 'fetch-active', 'fetch-value-replacement', 'fetch-value-trim',
        'fetch-value-replacement-ignore-case', 'fetch-html-replacement', 'fetch-html-replacement-ignore-case'];
    const specialFields = ['fetch-selector', 'fetch-parent-selector', 'fetch-bold-field',
        'fetch-exclude-selector', 'fetch-join-selector', 'fetch-format', 'fetch-value-replacement', 'fetch-html-replacement'];

    let time = 0, t = null;

    function fullBold(ev) {
        if (!ev.target.matches('.fetch-sentence-field')) {
            return
        }
        if (ev.type === 'mousedown') {
            time = 0;
            t = setInterval(() => {
                time += 1;
                clearInterval(t);
            }, 1000);
            return;
        }
        if (ev.type === 'mouseup') {
            if (time >= 1) {
                boldAll = true
                ev.preventDefault();
                ankiFetchClickFn(ev.target);
                boldAll = false;
            }
        }
        time = 0;
        clearInterval(t);
    }

    function getFetchItemEles() {
        return [...setting.children].slice(1);
    }

    function addOrDelBtn() {
        const fetchMap = {};
        const hadMap = {};
        for (const el of document.querySelectorAll('.fetch-sentence-field')) {
            let input = findParent(el, '.form-item').querySelector('.field-name,.sentence_field');
            hadMap[input.value] = el;
        }

        if (getFetchItemEles().length < 1) {
            let fetchItems = GM_getValue('fetch-items', [{...de}]);
            fetchItems.forEach(v => {
                if (!fetchMap.hasOwnProperty(v['fetch-to-field'])) {
                    fetchMap[v['fetch-to-field']] = [[v['fetch-active'], v]]
                } else {
                    fetchMap[v['fetch-to-field']].push([v['fetch-active'], v]);
                }
            });
        } else {
            for (const ele of document.querySelectorAll('.fetch-to-field')) {
                const active = findParent(ele, '.fetch-item').querySelector('.fetch-active').checked;
                if (!fetchMap.hasOwnProperty(ele.value)) {
                    fetchMap[ele.value] = [[active, ele]]
                } else {
                    fetchMap[ele.value].push([active, ele]);
                }
            }
        }


        Object.keys(fetchMap).map(k => {
            let active = false;
            const title = fetchMap[k].filter(v => v[0]).map(v => {
                active = true;
                return v[1] instanceof HTMLElement ? findParent(v[1], '.fetch-item').querySelector('.fetch-name').value : v[1]['fetch-name']
            });
            const input = document.querySelector(`:where(input.field-name,input.sentence_field)[value='${k}']`);
            if (hadMap.hasOwnProperty(k)) {
                delete hadMap[k];
            }
            const btn = input.parentElement.querySelector(`.fetch-sentence-field`);
            if (active && btn) {
                btn.title = `将提取${title.join(',')}`;
                return;
            }
            if (active && !btn) {
                const btn = document.createElement('button');
                btn.innerHTML = `⚓`;
                btn.className = 'fetch-sentence-field';
                btn.title = `将提取${title.join(',')} 右键选择:单个执行操作`;
                const op = findParent(input, '.form-item').querySelector('.field-operate');
                op && op.appendChild(btn);
                return;
            }
            btn && btn.remove();
        })
        Object.keys(hadMap).forEach(k => hadMap[k].remove());
    }

    function fetchActive(ev) {
        const box = ev.target;
        const parent = box.parentElement.parentElement;
        const inp = parent.querySelector('.fetch-field');
        const targetField = parent.querySelector('.fetch-to-field');
        addOrDelBtn();
        if (!inp.value) {
            Swal.showValidationMessage('提取的字段不能为空!');
            inp.focus();
            box.checked = false;
            return
        }
        if (!targetField.value) {
            Swal.showValidationMessage('提取到目标的字段不能为空!');
            targetField.focus();
            box.checked = false;
        }
        saveFetchItems();
    }

    let setting, boldAll = false;

    function setAllBold(value) {
        boldAll = value
    }

    function saveFetchItems() {
        const data = getFetchItemEles().map(item => convertFetchParam(item));
        data.length > 0 && GM_setValue('fetch-items', data);
    }

    function convertFetchParam(item) {
        const param = {};
        fetchFields.forEach(sel => {
            if (['fetch-num', 'fetch-data-handle'].includes(sel)) {
                param[sel] = parseInt(item.querySelector(`.${sel}`).value);
                return
            }
            if (['fetch-repeat', 'fetch-active', 'fetch-value-trim', 'fetch-html-replacement-ignore-case', 'fetch-value-replacement-ignore-case'].includes(sel)) {
                param[sel] = item.querySelector(`.${sel}`).checked;
                return
            }

            if (specialFields.includes(sel)) {
                param[sel] = item.querySelector(`.${sel}`).value;
                param[sel] = (param[sel]);
                return;
            }
            param[sel] = item.querySelector(`.${sel}`).value.trim();
        });
        return param
    }

    function replace(value, param) {
        if (!param['fetch-value-replacement'] || !value) {
            return value;
        }
        const arr = param['fetch-value-replacement'].split('@@');
        if (arr.length < 1) {
            return value
        }
        return arr.reduce((value, express) => {
            const exp = express.split('[=]');
            if (exp.length < 1) {
                return value;
            }
            const v = exp.length > 1 ? exp[1] : '';
            let flag = 'g';
            if (param['fetch-value-replacement-ignore-case']) {
                flag += 'i';
            }
            if (exp[0].includes('\\p{Script=')) {
                flag += 'u';
            }
            try {
                exp[0] = exp[0].replaceAll(`\\\\`, `\\`);
                value = value.replaceAll(new RegExp(exp[0], flag), v);
            } catch (e) {
                console.log(e);
                value = value.split(exp[0]).join(v);
            }

            return value
        }, value);
    }

    function buildRegular(words, flag, find = false) {
        let suffix = '';
        const w = [];
        const ends = find ? '.+?' : '.*?';
        const begin = find ? '\\w+?' : '\\w*?';
        words.forEach(word => {
            if (!word) {
                return
            }
            if (word.length <= 2) { //  || (word.length === 3 && !find)
                w.push(word);
                return;
            }
            if (word[word.length - 1] === '-') {
                const prefix = word.slice(0, -1);
                w.push(word + '.+?');
                if (!words.includes(prefix)) {
                    w.push(prefix + ends);
                }
                return
            }
            if (word === suffix) {
                suffix = '';
                w.push(word + ends);
                w.push(begin + word);
                return
            }
            if (word[0] === '-') {
                suffix = word.slice(1);
                if (!words.includes(suffix)) {
                    w.push(begin + suffix);
                }
                w.push(begin + word);
                return
            }
            w.push(word + ends);
        });
        return new RegExp(`\\b(${w.join('|')})\\b`, flag);
    }

    function eleBold(el, words, formats, boldAll) {
        if (el.childNodes.length < 1) {
            return 0;
        }
        const flag = 'ig';
        const wordReg = buildRegular(words, flag);
        const d = document.createElement('div');
        let replacedNum = 0;
        // wtf! loop nodes with for ...of none other than dynamic
        for (const node of [...el.childNodes]) {
            if (node.nodeType === node.TEXT_NODE) {
                const o = node.nodeValue;
                let n = node.nodeValue.replace(wordReg, formats);
                if (o !== n) {
                    d.innerHTML = n;
                    node.replaceWith(...d.childNodes)
                    replacedNum++;
                    if (!boldAll) {
                        break;
                    }
                    continue;
                }
                let wordsEx = [...words];
                while (true) {
                    wordsEx = wordsEx.filterAndMapX(v => v.length > 3 ? v.slice(0, -1) : false);
                    if (wordsEx.length < 1) {
                        break
                    }
                    const wordReg = buildRegular(wordsEx, flag, true);
                    n = node.nodeValue.replace(wordReg, formats);
                    if (o !== n) {
                        d.innerHTML = n;
                        node.replaceWith(...d.childNodes)
                        replacedNum++;
                        if (!boldAll) {
                            return replacedNum;
                        }
                        break;
                    }
                }
            }
            if (node.nodeType === node.ELEMENT_NODE) {
                replacedNum += eleBold(node, words, formats);
            }
        }
        return replacedNum;
    }

    function bold(sentence, boldFieldValue) {
        if (!boldFieldValue) {
            return sentence.innerHTML;
        }
        let words, format;
        if (Array.isArray(boldFieldValue)) {
            words = boldFieldValue[0].split(' ');
            format = boldFieldValue[1];
        } else {
            words = boldFieldValue.split(' ');
        }
        if (words.length < 1) {
            return sentence.innerHTML;
        }
        [...words].forEach(word => {
            const irs = lemmatizer.irregularConjugationOrPluralities(word);
            if (irs.length < 1) {
                return
            }
            irs.forEach(v => v[0].forEach(vv => words.push(vv)));
        })
        words = words.sort((a, b) => a.length <= b.length ? 1 : -1);
        const formats = format ? format.split('{$bold}').join('\$&') : '<b>\$&</b>';
        eleBold(sentence, words, formats, boldAll)

        return sentence.innerHTML;
    }

    const textTags = new Set(['INPUT', 'TEXTAREA']);


    function setValue(target, valElement, param, join = null, boldFieldValue = '') {
        let joinEle, joinRep;
        if (join) {
            joinEle = join.joinEle
            joinRep = join.joinRep
        }
        if (!valElement && !joinEle) {
            return
        }
        const joinParam = {
            'fetch-value-replacement-ignore-case': true,
            'fetch-value-replacement': joinRep
        }

        const format = param['fetch-format'], way = param['fetch-data-handle'], isRepeat = !param['fetch-repeat'];
        let onlyJoin = false;
        if (!valElement && joinEle) {
            valElement = joinEle;
            joinEle = null;
            onlyJoin = true;
        }

        const setInput = (input, value, isAppend, isRepeat) => {
            if (textTags.has(value.tagName)) {
                value = replace(value.value, param);
            } else {
                value = replace(value.innerText, param);
            }
            if (param['fetch-value-trim']) {
                value = value.trim();
            }
            if (format) {
                let join = '';
                if (joinEle) {
                    join = joinEle.innerText.trim();
                    join = replace(join, joinParam);
                }
                value = onlyJoin ? format.replaceAll('{$join}', value).replaceAll('{$value}', '') :
                    format.replaceAll('{$value}', value).replaceAll('{$join}', join);
            }
            if (param['fetch-value-trim'] && !isRepeat && input.value.includes(value.trim())) {
                return;
            }
            if (!isRepeat && input.value.includes(value)) {
                return;
            }
            input.value = isAppend ? (input.value + value) : value;
        }

        const setDiv = (div, value, isAppend, isRepeat) => {
            let v = value.innerText.trim();
            if (param['fetch-data-type'] === 'text' && v && !isRepeat && div.innerText.includes(v)) {
                return
            }

            if (param['fetch-data-type'] === 'html' && v && !isRepeat && div.innerHTML.includes(value.outerHTML)) {
                return
            }

            const set = (di) => {
                if (di.children.length > 0) {
                    isAppend ? [...di.children].forEach(v => div.appendChild(v)) : (div.innerHTML = di.innerHTML);
                    return;
                }
                if (isAppend) {
                    div.insertAdjacentHTML('afterend', di.innerHTML);
                    return;
                }
                div.innerHTML = di.innerHTML;
            }

            if (param['fetch-field'] === param['fetch-to-field']) {
                if (param['fetch-data-type'] === 'text') {
                    value.innerText = replace(value.innerText, param);
                } else {
                    value.innerHTML = replace(value.innerHTML, param);
                }
                value.innerHTML = bold(value, boldFieldValue);
                return;
            }

            const d = document.createElement('div');
            if (param['fetch-data-type'] === 'text') {
                d.innerHTML = replace(value.outerHTML, {
                    'fetch-value-replacement': param['fetch-html-replacement'],
                    'fetch-value-replacement-ignore-case': param['fetch-html-replacement-ignore-case'],
                })
                d.innerHTML = replace(d.innerText, param);
                d.innerHTML = bold(d, boldFieldValue);
            } else if (param['fetch-data-type'] === 'html') {
                d.innerHTML = replace(value.outerHTML, param);
                d.innerHTML = bold(d, boldFieldValue);
            }

            value = d.children.length > 0 ? d.children[0] : d;

            if (format) {
                let join = '';
                if (joinEle) {
                    join = joinEle.innerText.trim();
                    join = replace(join, joinParam);
                }
                let v;
                if (onlyJoin) {
                    v = format.replaceAll('{$join}', d.innerText.trim()).replaceAll('{$value}', '');
                } else {
                    v = format.replaceAll('{$value}', d.innerHTML).replaceAll('{$join}', join);
                }

                const di = document.createElement('div');
                di.innerHTML = v;
                if (!isRepeat && div.innerText.includes(di.innerText)) {
                    return
                }
                set(di);
                return;
            }
            if (joinEle) {
                const d = document.createElement('div');
                d.innerHTML = replace(joinEle.outerHTML, joinParam);
                isAppend ? (div.appendChild(d.children[0]) , div.appendChild(value)) : (div.innerHTML = d.innerHTML + value.outerHTML);
                return;
            }
            let html = value.outerHTML;
            if (value.className === 'spell-content') {
                html = value.innerHTML;
            }
            isAppend ? div.appendChild(value) : (div.innerHTML = html);
        }

        if (textTags.has(target.tagName)) {
            const value = target.value;
            if (way === 3 && value) {
                return;
            }
            setInput(target, valElement, way === 1, isRepeat);
            return;
        }
        setDiv(target, valElement, way === 1, isRepeat)
    }

    function fetchLimit(eles, num) {
        if (num <= 0) {
            return eles;
        }
        return [...eles].slice(0, num)
    }

    function findELeBySelector(t, sel, el) {
        if (!el) {
            return null;
        }
        if (t === 's') {
            return el.querySelector(sel);
        }
        let ele = el;
        do {
            if (!ele) {
                return null;
            }
            switch (t) {
                case 'p':
                    ele = ele.parentElement;
                    break;
                case 'ns':
                    ele = ele.nextElementSibling;
                    break
                case 'ps':
                    ele = ele.previousSibling;
                    break;
                default:
                    return null
            }
            if (ele && ele.matches(sel)) {
                return ele;
            }
        } while (ele)
        return ele;
    }

    function findEleByNum(t, num, el) {
        if (!el) {
            return null;
        }
        if (num < 1) {
            return null
        }
        let ele = el;
        do {
            if (!ele) {
                return null;
            }
            switch (t) {
                case 'p':
                    ele = ele.parentElement;
                    break;
                case 'ns':
                    ele = ele.nextElementSibling;
                    break
                case 'ps':
                    ele = ele.previousSibling;
                    break;
                default:
                    return null
            }

        } while (--num)
        return ele;
    }

    function parseSelector(expression, joinEle) {
        let ele = joinEle;
        for (const exp of expression.split('%')) {
            const arr = exp.split('@').map(v => v.trim());
            if (arr.length < 1 || !['s', 'ps', 'p', 'ns'].includes(arr[0])) {
                continue
            }
            ele = isNaN(parseInt(arr[1])) ? findELeBySelector(arr[0], arr.slice(1).join(''), ele) : findEleByNum(arr[0], arr[1], ele)
        }

        return ele === joinEle ? null : ele;

    }

    function removeEle(ele, selector) {
        if (!selector) {
            return
        }
        ele.querySelectorAll(selector).forEach(el => el.remove());
    }

    function inputTrim(target, param) {
        if (!textTags.has(target.tagName)) {
            return;
        }
        if (!param['fetch-value-trim']) {
            return
        }
        target.value = target.value.trim();
    }

    function parseJoin(valueEle, joinSelector, excludeSelector = '') {
        if (!joinSelector) {
            return null
        }
        let joinEle = parseSelector(joinSelector, valueEle);
        if (joinEle) {
            joinEle = joinEle.cloneNode(true);
            if (excludeSelector) {
                removeEle(joinEle, excludeSelector);
            }
        }
        return joinEle;
    }

    function fetchData(from, target, param, boldFieldValue) {
        from = from.parentElement;
        let joinRep = '', joinSelector = '', joinExclude = '';
        if (param['fetch-join-selector']) {
            const joinSelX = param['fetch-join-selector'].split('++');
            const joinSel = joinSelX[0].split('`');
            joinSelector = joinSel[0];
            if (joinSel.length > 1) {
                joinExclude = joinSel[1];
            }
            if (joinSelX.length > 1) {
                joinRep = joinSelX[1];
            }
        }
        if (!param['fetch-parent-selector']) {
            for (const el of fetchLimit(from.querySelectorAll(param['fetch-selector']), param['fetch-num'])) {
                const joinEle = parseJoin(el, joinSelector, joinExclude);
                let ele = el;
                if (param['fetch-field'] !== param['fetch-to-field']) {
                    ele = el.cloneNode(true);
                }
                removeEle(ele, param['fetch-exclude-selector']);
                setValue(target, ele, param, {joinEle, joinRep}, boldFieldValue);
            }
            return;
        }
        from.querySelectorAll(param['fetch-parent-selector']).forEach(parent => {
            for (const el of fetchLimit(parent.querySelectorAll(param['fetch-selector']), param['fetch-num'])) {
                let ele = el;
                if (param['fetch-field'] !== param['fetch-to-field']) {
                    ele = el.cloneNode(true);
                }
                removeEle(ele, param['fetch-exclude-selector']);
                const joinEle = parseJoin(el, joinSelector, joinExclude);
                setValue(target, ele, param, {joinEle, joinRep}, boldFieldValue);
            }
        })
    }


    function ankiFetchData(param, target = null, from = null) {
        if (!target) {
            for (const button of document.querySelectorAll('.fetch-sentence-field')) {
                if (button.parentElement.parentElement.querySelector('.field-name,.sentence_field').value.trim() === param['fetch-to-field']) {
                    target = findParent(button, '.form-item,.sentence_setting').querySelector('.spell-content,.field-value');
                    break
                }
            }
        }
        if (!from) {
            from = [...document.querySelectorAll('.field-name,.sentence_field')].filter(el => el.value === param['fetch-field']);
            from = from ? findParent(from[0], '.form-item,.sentence_setting').querySelector('.spell-content,.field-value') : null;
        }
        if (!target || !from) {
            return
        }

        const bold = parseBoldFormat(param);
        fetchData(from, target, param, bold);
        inputTrim(target, param);
    }

    function getAnkiFetchParams(targetField = '', activeFilter = true) {
        let params;
        if (getFetchItemEles().length < 1) {
            params = GM_getValue('fetch-items')
        } else {
            params = [...document.querySelectorAll('.fetch-to-field')].map(el => {
                const item = findParent(el, '.fetch-item');
                return convertFetchParam(item);
            })
        }
        if (!params || params.length < 1) {
            return;
        }
        if (!targetField) {
            return params;
        }
        return params.filter(param => {
            if (activeFilter) {
                return param['fetch-active'] && param['fetch-to-field'] === targetField
            }
            return param['fetch-to-field'] === targetField
        });
    }

    function ankiFetchClickFn(button) {
        const targetField = button.parentElement.parentElement.querySelector('.sentence_field,.field-name').value.trim();
        const targetEle = button.parentElement.parentElement.querySelector('.spell-content,.field-value');
        const arr = getAnkiFetchParams(targetField, true);
        let from = [...document.querySelectorAll('.field-name,.sentence_field')].filter(el => el.value === arr[0]['fetch-field']);
        from = from ? findParent(from[0], '.form-item,.sentence_setting').querySelector('.spell-content,.field-value') : null;
        if (!from) {
            return;
        }
        arr.forEach(v => ankiFetchData(v, targetEle, from));
    }


    function parseBoldFormat(param) {
        if (!param['fetch-bold-field']) {
            return ''
        }
        let boldFieldValue = '';
        const fields = param['fetch-bold-field'].split('@@');
        const input = document.querySelector(`input.field-name[value='${fields[0]}']`);
        if (!input) {
            return boldFieldValue;
        }
        const ip = input.nextElementSibling;
        if (ip && ip.matches('input.field-value')) {
            boldFieldValue = ip.value;
            if (fields.length > 1) {
                const f = fields[1].split('%%');
                if (f.length === 1 && f[0].includes('{$bold}')) {
                    return [boldFieldValue, f[0]];
                }
                return [boldFieldValue.split(f[0].replaceAll('`', '')).join(' '), f[1]];
            }
        }
        return boldFieldValue;
    }

    const mapTitle = {
        'fetch-name': '名称,只作为标识',
        'fetch-field': '提取的字段',
        'fetch-to-field': '提取到目标字段',
        'fetch-selector': '提取值的选择器',
        'fetch-parent-selector': '父选择器',
        'fetch-exclude-selector': '提取值需要排除的选择器',
        'fetch-join-selector': '组合选择器',
        'fetch-format': '提取的格式,为空为原值,{$join}为组合选择器的值, {$value}为提取的值',
        'fetch-data-handle': '提取到后的操作',
        'fetch-data-type': '提取类型',
        'fetch-repeat': '是否去重',
        'fetch-bold-field': htmlSpecial('加粗的字段,如有多个值,可以指定分隔符如 正面@@`,`%%<b>{$bold}</b> %%后为格式'),
        'fetch-num': '提取的数量,默认0为全部',
        'fetch-value-replacement': '提取的值去除或替换,[=]前后分为表示要替换的值和替换值,多个用@@分隔,支持正则, 如 去掉·和将。替换为. 为 ·@@。[=].',
        'fetch-html-replacement': 'html去除或替换,[=]前后分为表示要替换的值和替换值,多个用@@分隔,支持正则, 如 去掉·和将。替换为. 为 ·@@。[=].,在提取为值之前执行',
        'fetch-value-trim': '提取的值去除首尾空白符如空格等',
        'fetch-value-replacement-ignore-case': '是否忽略大小写',
        'fetch-html-replacement-ignore-case': '是否忽略大小写',
        'fetch-active': '是否启用这个操作项',
        'fetch-delete': '删除此项',
        'fetch-copy': '复制此项',
        'fetch-add': '在此项后台添加一个操作项',
    };
    const de = {};
    Object.keys(mapTitle).forEach(k => {
        de[k] = '';
        de['fetch-num'] = 0;
        de['fetch-repeat'] = true;
        de['fetch-active'] = false;
        de['fetch-data-handle'] = 1;
        de['fetch-data-type'] = 'text';
        de['fetch-value-trim'] = false;
        de['fetch-value-replacement-ignore-case'] = false;
        de['fetch-html-replacement-ignore-case'] = false;
    });

    function buildFetchItem(data = null) {
        specialFields.forEach(v => data[v] = data[v] ? htmlSpecial(data[v]) : '');
        const div = document.createElement('div');
        div.innerHTML = `
                <div class="fetch-item" draggable="true">
                    <span class="fetch-box">
                           <input class="fetch-name" value="${data['fetch-name']}" title="${mapTitle['fetch-name']}" placeholder="${mapTitle['fetch-name']}">
                    </span>
                    <span class="fetch-box">
                        <dd class="fetch-dd">
                            <input name="fetch-field" value="${data['fetch-field']}" class="fetch-field" title="${mapTitle['fetch-field']}" placeholder="${mapTitle['fetch-field']}">
                            <input name="fetch-to-field" value="${data['fetch-to-field']}" class="fetch-to-field" title="${mapTitle['fetch-to-field']}" placeholder="${mapTitle['fetch-to-field']}">
                        </dd>
                        <dd class="fetch-dd">
                            <input name="fetch-selector" value="${data['fetch-selector']}" class="fetch-selector" title="${mapTitle['fetch-selector']}" placeholder="${mapTitle['fetch-selector']}">
                            <input name="fetch-parent-selector" value="${data['fetch-parent-selector']}" class="fetch-parent-selector" title="${mapTitle['fetch-parent-selector']}" placeholder="${mapTitle['fetch-parent-selector']}">
                        </dd>
                        <dd class="fetch-dd">
                            <input name="fetch-exclude-selector" value="${data['fetch-exclude-selector']}" class="fetch-exclude-selector" title="${mapTitle['fetch-exclude-selector']}" placeholder="${mapTitle['fetch-exclude-selector']}">
                            <input name="fetch-join-selector" value="${data['fetch-join-selector']}" class="fetch-join-selector" title="${mapTitle['fetch-join-selector']}" placeholder="${mapTitle['fetch-join-selector']}">
                        </dd>
                        <dd class="fetch-dd">
                            <input name="fetch-format" value="${data['fetch-format']}" class="fetch-format" title="${mapTitle['fetch-format']}" placeholder="${mapTitle['fetch-format']}">
                            <select name="fetch-data-handle" class="fetch-data-handle" title="${mapTitle['fetch-data-handle']}">
                                ${buildOption([['1', '追加'], ['2', '覆盖'], ['3', '不处理']], data['fetch-data-handle'].toString(), 0, 1)}
                            </select>                          
                        </dd>
                        <dd class="fetch-dd">
                               <input name="fetch-bold-field" value="${data['fetch-bold-field']}" class="fetch-bold-field" title="${mapTitle['fetch-bold-field']}" placeholder="${mapTitle['fetch-bold-field']}">
                               <input name="fetch-num" step="1" min="0" value="${data['fetch-num']}" class="fetch-num" type="number" title="${mapTitle['fetch-num']}" placeholder="${mapTitle['fetch-num']}">
                               <input type="checkbox" ${data['fetch-repeat'] ? 'checked' : ''} name="fetch-repeat" class="fetch-repeat" title="${mapTitle['fetch-repeat']}" placeholder="${mapTitle['fetch-repeat']}">
                        </dd>
                        <dd class="fetch-dd">
                               <input name="fetch-value-replacement" value="${data['fetch-value-replacement']}" class="fetch-value-replacement" title="${mapTitle['fetch-value-replacement']}" placeholder="${mapTitle['fetch-value-replacement']}">
                               <input type="checkbox" ${data['fetch-value-trim'] ? 'checked' : ''} name="fetch-value-trim" class="fetch-value-trim" title="${mapTitle['fetch-value-trim']}" placeholder="${mapTitle['fetch-value-trim']}">
                               <input type="checkbox" ${data['fetch-value-replacement-ignore-case'] ? 'checked' : ''} name="fetch-value-replacement-ignore-case" class="fetch-value-replacement-ignore-case" title="${mapTitle['fetch-value-replacement-ignore-case']}" placeholder="${mapTitle['fetch-value-replacement-ignore-case']}">
                               <select name="fetch-data-type" class="fetch-data-type" title="${mapTitle['fetch-data-type']}">
                                ${buildOption([['text', '文本'], ['html', 'html']], data['fetch-data-type'], 0, 1)}
                            </select>   
                        </dd>
                        <dd class="fetch-dd">
                            <input name="fetch-html-replacement" value="${data['fetch-html-replacement']}" class="fetch-html-replacement" title="${mapTitle['fetch-html-replacement']}" placeholder="${mapTitle['fetch-html-replacement']}">
                            <input type="checkbox" ${data['fetch-html-replacement-ignore-case'] ? 'checked' : ''} name="fetch-html-replacement-ignore-case" class="fetch-html-replacement-ignore-case" title="${mapTitle['fetch-html-replacement-ignore-case']}" placeholder="${mapTitle['fetch-html-replacement-ignore-case']}">
                        </dd>
                    </span>     
                    <span class="fetch-box">
                        <input type="checkbox" ${data['fetch-active'] ? 'checked' : ''} name="fetch-active" class="swal2-checkbox fetch-active" title="${mapTitle['fetch-active']}" placeholder="${mapTitle['fetch-active']}">
                        <div class="fetch-buttons">
                            <button class="fetch-delete" title="${mapTitle['fetch-delete']}">➖</button>
                            <button class="fetch-copy" title="${mapTitle['fetch-copy']}">🖇</button>
                            <button class="fetch-add" title="${mapTitle['fetch-add']}">➕</button>
                        </div>
                    </span>                  
                </div>
        `;
        div.querySelector('.fetch-active').addEventListener('change', fetchActive);
        return div.children[0];
    }


    PushHookAnkiHtml((ankiContainer) => {
        const div = document.createElement('div');
        div.className = 'form-item fetch-sentence-container';
        div.innerHTML = `
            <div class="fetch-opera">
                <label for="fetch" class="form-label">简易字段加工处理器</label>
                <input type="checkbox" class="swal2-checkbox" name="fetch" id="fetch" title="显示设置">
                <button class="fetch-all" title="一键执行全部操作">🕸️</button>
                <button class="fetch-import fetch-hidden" title="导入">🚚</button>
                <button class="fetch-export fetch-hidden" title="导出">🚍</button>
                <input type="file" accept="text/plain, application/json" class="fetch-file fetch-hidden">
            </div>
            <div class="select-setting"><select class="fetch-item-select fetch-hidden"></select></div>
        `;

        setting = div.querySelector('.select-setting');
        let currentItem;

        const startDrag = (e) => {
            e.dataTransfer.effectAllowed = 'move';
            currentItem = e.target;
            currentItem.classList.add('moving');
        };
        const enterDrag = (e) => {
            e.preventDefault();
            const children = getFetchItemEles();
            if (e.target === currentItem || children.length <= 1 || e.target === setting || !children.includes(e.target)) {
                return
            }
            let liArray = Array.from(setting.childNodes);
            let currentIndex = liArray.indexOf(currentItem);
            let targetindex = liArray.indexOf(e.target);
            if (currentIndex < targetindex) {
                setting.insertBefore(currentItem, e.target.nextElementSibling);
            } else {
                setting.insertBefore(currentItem, e.target);
            }
        };
        const endDrag = (e) => {
            currentItem.classList.remove('moving');
            saveFetchItems();
        };
        const overDrag = (e) => {
            e.preventDefault();
        };
        const turnDrag = (onoff) => {
            setting.querySelectorAll('.fetch-item').forEach(item => item.draggable = onoff);
        };
        setting.addEventListener('mousedown', (ev) => {
            if (ev.target.tagName === 'INPUT') {
                turnDrag(false);
            }
        });
        setting.addEventListener('mouseup', (ev) => {
            if (ev.target.tagName === 'INPUT') {
                turnDrag(true);
            }
        });
        setting.addEventListener('dragstart', startDrag);
        setting.addEventListener('dragenter', enterDrag);
        setting.addEventListener('dragend', endDrag);
        setting.addEventListener('dragover', overDrag);


        ankiContainer.querySelector('#auto-sentence').parentElement.insertAdjacentElement('afterend', div);
    });
    return {
        ankiFetchClickFn, ankiFetchData, setAllBold, getAnkiFetchParams, arrayDiff: diff
    }
})();