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/1805461/ankienhance.js

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

;const {ankiFetchClickFn, ankiFetchData, getAnkiFetchParams, arrayDiff, superFetchHook, setEleDrag} = (() => {
    PushHookAnkiStyle(GM_getResourceText('extract-sentence'));

    PushHookAnkiDidRender(freshBtns);

    function changeAddDelBtn(ev, fn) {
        fn && fn(ev);
        freshBtns();
    }

    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('sequentially-fetch', '', ev => GM_setValue('sequentially-fetch', ev.target.checked));
    PushExpandAnkiInputButton('fetch-add', '', (e) => {
        findParent(e.target, '.fetch-item').insertAdjacentElement('afterend', actionHelper.buildFetchItem({}));
    });
    PushExpandAnkiInputButton('fetch-export', '', () => eventFn.export(),
        '', evt => {
            evt.preventDefault();
            eventFn.export(
                [...setting.querySelectorAll('.fetch-item:not(.fetch-hidden)')].map(formProcessor.convertFetchParam)
            )
        });
    const importFn = ev => ev.target.parentElement.querySelector('.fetch-file').click();
    PushExpandAnkiInputButton('fetch-import', '', importFn, '', ev => {
        ev.preventDefault();
        ev.target.dataset['new'] = 'true';
        importFn(ev);
    });

    /**
     *
     * @param a arr
     * @param b arr
     * @param fn
     * @returns {*} a's item not in b
     */
    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])
    }

    const eventFn = {
        contextMenuAction(ev) {
            const button = ev.target, input = button.parentElement.parentElement.querySelector('.field-name');
            const targetField = input.value.trim(), isText = actionHelper.isTextNode(input.nextElementSibling);
            const targetEle = button.parentElement.parentElement.querySelector('.spell-content,.field-value');
            const arr = getAnkiFetchParams(targetField, false).filterAndMapX(actionHelper.filterButton(isText));
            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'];
            });
            opts.unshift(['', '选择一个操作']);
            sel.innerHTML = buildOption(opts, '', 0, 1);
            const fn = (ev) => {
                if (sel.value) {
                    actionHelper.executeAction(map[sel.value], actionHelper.getFromEle(map[sel.value], targetEle), 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.replaceWith(sel);
        },
        dragEle: {},

        changedEleSelector: '.swal2-popup, #shadowFields > ol, ol .form-item:has( .sentence_setting) :where(.spell), .form-item .spell-content',

        export(params = getAnkiFetchParams('', false)) {
            const data = JSON.stringify(params);
            const current = new Date();
            // wtf time format
            download(`fetch-rule.${current.getFullYear()}-${current.getMonth() + 1}-${current.getDate()}.${current.getHours()}.${current.getMinutes()}.${current.getSeconds()}.total.${params.length}.rows.json`, data);
        },
        showProcessor(ev) {
            const selector = '.fetch-import,.fetch-export';
            if (!ev.target.checked) {
                saveFetchItems();
                freshBtns();
                setting.children[0].classList.add('fetch-hidden');
                getFetchItemEles().map(e => e.remove());
                ev.target.parentElement.querySelectorAll(selector).forEach(btn => btn.classList.add('fetch-hidden'))
                return
            }
            let fetchItems = GM_getValue('fetch-items', [{}]);
            fetchItems.forEach(item => setting.appendChild(actionHelper.buildFetchItem(item)));
            if (GM_getValue('fetch-display-type', 1) === 2) {
                const arr = Object.groupBy(fetchItems, item => buttonField(item)) ?? [];
                const options = [];
                const nb = '&ensp;'.repeat(6);
                Object.keys(arr).forEach(k => {
                    options.push([k, k, {'data-names': arr[k].map(m => m['fetch-name']).join(',')}]);
                    arr[k].forEach(m => options.push([m['fetch-name'], nb + m['fetch-name']]));
                });
                setting.children[0].innerHTML = buildOption(options, options[1][0], 0, 1, 2);
                setting.children[0].classList.remove('fetch-hidden');
                setting.children.length > 2 && [...setting.children].slice(2).forEach(e => e.classList.add('fetch-hidden'));
            }
            const fns = [...setting.querySelectorAll('.fetch-replacement-items')].map(el => setEleDrag(el, 'li'));
            eventFn.dragEle['replaceItem'] = onOff => fns.forEach(fn => fn(onOff));
            eventFn.dragEle.replaceItem(true);
            eventFn.dragEle['fetch-item'] = setEleDrag(setting, '.fetch-item');
            eventFn.dragEle['fetch-item'](true);
            eventFn.dragEle['super-fetch-item'] = setEleDrag(setting, '.super-fetch-item');
            eventFn.dragEle['super-fetch-item'](true);
            ev.target.parentElement.querySelectorAll(selector).forEach(btn => btn.classList.remove('fetch-hidden'))
        },
        async importFn(ev) {
            const file = ev.target.files[0];
            const btn = ev.target.parentElement.querySelector('.fetch-import');
            const refresh = btn.dataset?.['new'];
            delete btn.dataset?.['new'];
            if (!file) {
                Swal.showValidationMessage(mapTitle['no file']);
                return
            }
            const items = await file.text().then(JSON.parse);
            if (!items || items.length < 1 || !items[0]?.['fetch-name']) {
                Swal.showValidationMessage(`can't parse rule file`);
                return
            }
            let newRule = [];
            if (refresh) {
                newRule = items;
                getFetchItemEles().forEach(el => el.remove());
            } else {
                const hadRule = getAnkiFetchParams('', false);
                newRule = diff(items, hadRule, (a, b) => JSON.stringify(a) === JSON.stringify(b));
            }

            if (newRule.length < 1) {
                Swal.showValidationMessage(mapTitle['redundantly import!']);
                return
            }
            const names = [];
            newRule.forEach(item => {
                const t = actionHelper.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);
            }
        },
        addTplFn: {
            tpl: name => templateHelper.buildTemplateHTML(name, {}),

            tplFn: fn => eventFn.addTplFn?.[fn]({}),

            replacement(data = {}) {
                return actions.handlers.replacement.getReplacementItem(data);
            },
            fetch(data) {
                return actions.handlers.fetch.getFetchItem(data);
            }
        },

        add(ev) {
            const el = ev.target.dataset?.target ? findParent(ev.target, ev.target.dataset.target) : ev.target.parentElement;
            for (const name of ['tplFn', 'tpl']) {
                if (ev.target.dataset?.[name]) {
                    const t = this.addTplFn[name](ev.target.dataset?.[name]);
                    el.insertAdjacentElement('afterend', t[0]);
                    return;
                }
            }
            const em = el.cloneNode(true);
            em.querySelectorAll('input,select').forEach(ele => {
                const fn = {
                    INPUT(ele) {
                        ele.value = '';
                        if (ele.type === 'checkbox') {
                            ele.checked = false;
                        }
                    },
                    SELECT(ele) {
                        ele.value = ele.children[0].value
                    },
                    TEXTAREA(ele) {
                        ele.value = ''
                    }
                };
                fn?.[ele.nodeName] && fn[ele.nodeName](ele);
            });
            el.insertAdjacentElement('afterend', em);
        },
        copy(ev) {
            ev.preventDefault();
            const el = ev.target.dataset?.target ? findParent(ev.target, ev.target.dataset.target) : ev.target.parentElement;
            el.insertAdjacentElement('afterend', el.cloneNode(true));
        },
        remove(ev) {
            ev.target.dataset?.target ?
                findParent(ev.target, ev.target.dataset.target)?.remove()
                : ev.target.parentElement.remove();
        }
    };

    PushHookAnkiChange('.fetch-file', ev => eventFn.importFn(ev));
    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);
    }, '', evt => eventFn.contextMenuAction(evt));


    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);
        a.remove();
    }

    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 fn = (name) => {
            const t = setting.querySelector(`.fetch-name[value='${name}']`);
            if (!t) {
                return
            }
            findParent(t, '.fetch-item').classList.remove('fetch-hidden');
        };
        const hidden = () => setting.querySelectorAll('.fetch-item:not(.fetch-hidden)').forEach(e => e.classList.add('fetch-hidden'));

        const el = ev.target.querySelector(`option[value='${ev.target.value === '*' ? '\\*' : ev.target.value}']`);
        if (el.dataset.hasOwnProperty('names')) {
            hidden()
            el.dataset.names.split(',').forEach(v => {
                fn(v);
            })
            return
        }
        hidden();
        fn(ev.target.value);
    });


    // show extract processor
    PushHookAnkiChange('#fetch.swal2-checkbox', ev => eventFn.showProcessor(ev));

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

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


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


    function buttonField(item) {
        return item?.['fetch-to-field'] ? item['fetch-to-field'] : item['fetch-field'];
    }

    function addBtn(input, items) {
        const title = items.filterAndMapX(item => item['fetch-active'] ? item['fetch-name'] : false).join(',');
        const btn = document.createElement('button');
        btn.classList.add('fetch-sentence-field');
        btn.title = title ? title + ' ' + mapTitle['right-operate'] : mapTitle['right-operate'];
        btn.innerHTML = '⚓';
        findParent(input, '.form-item')
            .querySelector('.field-operate')
            .insertAdjacentElement('beforeend', btn);
    }

    function freshBtns() {
        const items = getAnkiFetchParams() ?? [];
        if (items.length < 1) {
            return
        }
        const fetchMap = {};
        document.querySelectorAll('.fetch-sentence-field').forEach(el => el.remove());
        const generic = [];
        items.forEach(item => {
            if (item['fetch-field'] === '*') {
                generic.push(item);
                return
            }
            const field = buttonField(item);
            if (!fetchMap?.[field]) {
                fetchMap[field] = [];
            }
            fetchMap[field].push(item);
        });

        document.querySelectorAll('.field-name').forEach(input => {
            const isText = actionHelper.isTextNode(input.nextElementSibling);
            const field = input.value;
            if (!fetchMap?.[field] && generic.length < 1) {
                return
            }
            const genericArr = generic.filterAndMapX(actionHelper.filterButton(isText));
            if (!fetchMap?.[field]) {
                addBtn(input, genericArr);
                return;
            }
            addBtn(input, [...fetchMap[field].filterAndMapX(actionHelper.filterButton(isText)), ...genericArr]);
        });
    }

    function fetchActive(ev) {
        freshBtns();
        saveFetchItems();
    }

    let setting;

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


    const formProcessor = {
        getFormValue(form, param = {}, selector = 'input:not([data-batch] input),select:not([data-batch] select),textarea:not([data-batch] textarea)') {
            [...form.querySelectorAll(selector)].forEach(el => {
                const k = el.name;
                let v = el.value;
                if (el.type === 'number' && !el.dataset?.float) {
                    v = parseInt(v);
                }
                if (el.type === 'checkbox') {
                    v = el.checked;
                }
                param[k] = v;
            });
            return param;
        },
        convertFetchParam(item) {
            const data = formProcessor.getFormValue(item), t = data['operate-type'];
            actions.handlers[t]?.form?.(item, data);
            return data;
        }
    };


    const log = GM_getValue('dev', window?.['dev']) ? console.log.bind(window.console) : (...args) => {
    };


    const mapTitle = {
        'no file': '没有文件!',
        'fold-or-unfold': '折叠或展开子项',
        'handleElement': '处理元素',
        'handleElement-desc': '只作用于富文本字段,处理指定选择器对应的元素',
        'handle': '处理',
        'concatenation': '拼接',
        'multiple_child': '子项按组查询(queryAll)',
        'redundantly import': '无需导入!',
        'super html extract and process processor': '超级html提取加工处理器',
        "can't parse rule file": '不能解析规则文件!',
        'import': '左键增量导入,右键清空原数据后导入',
        'export': '左键全部导出,右键导出显示的记录',
        'fetch': '抓取',
        'escapeHTML': '转义HTML特殊实体',
        'unescapeHTML': '去除HTML特殊实体',
        'parseTemplate': '解析模板字符串',
        'templateVar': '模板字符串,可以为变量,相当于提取解析模板',
        'toUpperCase': '转成大写',
        'toLowerCase': '转成小写',
        'separator': '分隔符',
        'cached': '缓存该值(只查询一次)',
        'right-operate': '右键选择执行一个操作',
        'keep-parent': '当子项不存在时取父项',
        'do-all': '一键执行全部操作',
        'replacement': '替换',
        'tag': '打标签',
        'tag-desc': '只作用于富文本字段上,当有满足指定选择器时,打上对应标签',
        'fetch-name': '名称,只作为标识',
        'operate-type': '操作类型',
        'fetch-field': '提取的字段',
        'fetch-to-field': '提取到目标字段',
        'sequentially-fetch': '只对抓取操作项有效,尝试按内容顺序抓取,可能有性能及其它不未知问题',
        'parent-super-name': '父提取值的标识名',
        'fetch-selector': '选择器,多个时,前一个为后一个的父选择器,最后一个为锚选择器',
        'is_multiple': '是否有多个',
        'value-selector': '值选择器,值可为 parent child doc selector (p|ps|ns)@selector [%(p|ps|ns)@selector1] ...',
        'fetch-format': '提取的格式,可以使用{自身标识(即提取的值)或子项标识},为空为时默认为 {自身标识}, ',
        'fetch-data-handle': '提取到后的操作',
        'fetch-data-type': '提取类型',
        'fetch-repeat': '不重复',
        'fetch-num': '提取的数量,默认0为全部',
        'fetch-value-trim': '提取的值去除首尾空白符如空格等',
        'tag-selector': '标签的选择器',
        'fetch-tag': '设置的标签',
        'fetch-active': '是否启用这个操作项',
        'fetch-delete': '删除此项',
        'fetch-copy': '复制此项',
        'fetch-add': '在此项后台添加一个操作项',
        'super-fetch-name': '提取值的唯一标识名,可以作为变量名',
        'default-value': '默认值,可使用变量{标识名1[|标识名2]...}',
        'replace_target_type': '替换目标类型',
        'text': '文本',
        'add': '左键添加一个空白项,右键复制当前项',
        'innerHTML': 'innerHTML',
        'outerHTML': 'outerHTML',
        'searchValue': '替换或删除的目标,文本值或选择器',
        'replaceValue': '替换的值,当为删除时值为选择器',
        'remove element': '删除元素',
        'replace_regex_pattern': '正则替换模式如果是正则替换的化,为空则为普通替换',
        'cover': '覆盖',
        'append': '追加',
        'none': '啥都不干',
        'htmlElement': '元素',
    };




    function ankiFetchData(param, target = null, from = null) {
        if (!target) {
            target = actionHelper.getDestEle(param);
        }
        if (!from) {
            from = actionHelper.getFieldElement(param['fetch-field'], target);
        }

        if (target && from) {
            actions.dispatchAction(param, from, target);
            return
        }
        log(param);
    }

    function getAnkiFetchParams(targetField = '', activeFilter = true) {
        const params = getFetchItemEles().length < 1 ? GM_getValue('fetch-items') : getFetchItemEles().map(formProcessor.convertFetchParam);
        if (!params || params.length < 1) {
            return;
        }
        if (!targetField) {
            return params;
        }
        return params.filter(param => {
            const field = buttonField(param);
            if (activeFilter) {
                return param['fetch-active'] && (field === '*' || field === targetField);
            }
            return field === '*' || field === targetField;
        });
    }

    function ankiFetchClickFn(button) {
        const triggerField = button.parentElement.parentElement.querySelector('.field-name').value.trim();
        const param = getAnkiFetchParams(triggerField, true);
        if (param.length < 1) {
            return;
        }
        const sequence = GM_getValue('sequentially-fetch', false);
        if (!sequence) {
            param.forEach(item => actionHelper.executeAction(item));
            return;
        }
        const fetchItems = param.filterAndMapX(item => item['operate-type'] === 'fetch' ? item : false);
        if (!fetchItems || fetchItems?.length < 1) {
            return;
        }
        const from = actionHelper.getFieldElement(fetchItems[0]['fetch-field']);
        [...from.children].forEach(el => fetchItems.forEach(item => actionHelper.executeAction(item, el)));
    }

    const allowFn = {
        htmlSpecial, leftTrim, rightTrim, trims,
        checked(value) {
            return value ? ' checked ' : '';
        },
        buildOption,
        lang(name) {
            return htmlSpecial(mapTitle?.[name] ?? name);
        }
    };

    function leftTrim(s, symbol) {
        if (!s || !symbol) {
            return s;
        }
        for (const str of symbol.split('')) {
            if (s[0] === str) {
                s = s.substring(1);
                return s
            }
        }
        return s;
    }

    function rightTrim(s, symbol) {
        if (!s || !symbol) {
            return s;
        }
        for (const str of symbol.split('')) {
            if (s.length >= 1 && str === s[s.length - 1]) {
                s = s.substring(0, s.length - 1);
                return s
            }
        }
        return s;
    }

    function trims(s, symbol = `('")`) {
        return rightTrim(leftTrim(s, symbol), symbol);
    }

    function getVarVal(vars, express, defaults = '') {
        if (!express.includes('.')) {
            return vars?.[express] ?? defaults;
        }
        for (const name of express.split('.')) {
            if (!vars?.[name]) {
                return defaults;
            }
            vars = vars[name];
        }
        return vars;
    }


    const handleOp = {'append': mapTitle['append'], 'cover': mapTitle['cover'], 'none': mapTitle['none']};
    const operations = {fetch: mapTitle['fetch'], handle: mapTitle['handle']};
    const opType = {
        'text': mapTitle['text'],
        'remove element': mapTitle['remove element'],
        'innerHTML': mapTitle['innerHTML'],
        'outerHTML': mapTitle['outerHTML'],
        'toUpperCase': mapTitle['toUpperCase'],
        'toLowerCase': mapTitle['toLowerCase'],
        'parseTemplate': mapTitle['parseTemplate'],
        'escapeHTML': mapTitle['escapeHTML'],
        'unescapeHTML': mapTitle['unescapeHTML'],
    }, htmlType = {
        'text': mapTitle['text'],
        'innerHTML': mapTitle['innerHTML'],
        'outerHTML': mapTitle['outerHTML'],
        'htmlElement': mapTitle['htmlElement'],
    };
    const templateHelper = {
        templateFnHook: {},
        templateCache: {},
        replaceRex: /\{\{(.*?)}}/g,
        createElement(tag, attrs = {}) {
            const el = document.createElement(tag);
            Object.keys(attrs).forEach(k => el[k] = attrs[k]);
            return el;
        },
        buildTemplateHTML(template, vars = {}, ele = document.createElement('div')) {
            let t = this.templateCache?.[template] ?? '';
            if (!t) {
                t = GM_getResourceText(template) ?? '';
                this.templateCache[template] = t;
            }
            if (!t) {
                return t
            }
            t = t.replace(this.replaceRex, (substring, name) => {
                const names = name.split('|');
                let val = getVarVal(vars, names[0]);
                if (names.length < 2) {
                    return val;
                }
                for (let fn of names.splice(1)) {
                    if (fn === 'lang') {
                        return allowFn.lang(names[0]);
                    }
                    const fns = fn.split('(');
                    const param = [];
                    if (fns.length > 1) {
                        fn = fns[0].trim();
                        trims(fns[1], ')')
                            .split(',')
                            .forEach(a =>
                                (a = a.trim(), param.push(getVarVal(vars, a, trims(a))))
                            );
                    }
                    if (val?.[fn]) {
                        val = val[fn](...param);
                        continue;
                    }

                    if (!allowFn?.[fn]) {
                        return val;
                    }
                    val = allowFn[fn](val, ...param);
                }
                return val;
            });
            ele.innerHTML = t;
            ele.querySelectorAll('template').forEach(tpl => {
                const t = tpl.innerHTML;
                if (vars?.[t]) {
                    if (vars[t] instanceof Node) {
                        tpl.replaceWith(vars[t]);
                    } else if (Array.isArray(vars[t]) || vars[t] instanceof NodeList) {
                        tpl.replaceWith(...vars[t]);
                    }
                    return;
                }
                const name = t.split('|');
                if (name.length < 2) {
                    tpl.replaceWith(this.buildTemplateHTML(name[0]))
                    return
                }
                tpl.replaceWith(this.buildTemplateHTML(name[0], name[1] === '.' ? vars : getVarVal(vars, name[1], null)));
            });
            if (this.templateFnHook?.[template]) {
                this.templateFnHook[template](ele, vars);
            }
            return ele.children.length > 1 ? ele : ele.children[0];
        },


    };

    function setEleDrag(ele, selector, config = {}) {
        let currentItem;
        const turnDrag = onoff => ele.querySelectorAll(selector).forEach(item => item.draggable = onoff)
        const evenFn = {
            dragstart(e) {
                e.dataTransfer.effectAllowed = 'move';
                currentItem = e.target;
                currentItem.classList.add('moving');
            },
            dragenter(e) {
                e.preventDefault();
                const children = [...ele.querySelectorAll(selector)];
                if (e.target === currentItem || children.length <= 1) {
                    return
                }
                if (!e.target.classList.contains('moving') && !currentItem?.classList?.contains('moving')) {
                    //log(e.target, currentItem)
                    return;
                }
                const cur = children.indexOf(currentItem), tar = children.indexOf(e.target);
                if (cur < 0 || tar < 0) {
                    //log(e.target, tar, '---------', currentItem, cur);
                    return;
                }
                e.target.insertAdjacentElement(tar > cur ? 'afterend' : 'beforebegin', currentItem);
            },
            dragend(e) {
                currentItem.classList.remove('moving');
                saveFetchItems();
            },
            dragover(e) {
                e.preventDefault();
            },
            mousedown(ev) {
                if (ev.target.tagName === 'INPUT') {
                    turnDrag(false);
                }
            },
            mouseup(ev) {
                if (ev.target.tagName === 'INPUT') {
                    turnDrag(true);
                }
            },
            ...config
        }
        return on => {
            if (on) {
                turnDrag(true);
                Object.keys(evenFn).forEach(name => ele.addEventListener(name, evenFn[name]));
                return
            }
            Object.keys(evenFn).forEach(v => ele.removeEventListener(v, evenFn[v]));
            turnDrag(false);
        }
    }

    PushHookAnkiChange('.replace_target_type', evt => {
        const input = evt.target.parentElement.querySelector('.replace_regex_pattern');
        if (Object.keys(opType).includes(evt.target.value) && (!input || input.nodeName !== 'INPUT')) {
            const inp = document.createElement('input');
            inp.type = 'text';
            inp.name = 'replace_regex_pattern';
            inp.className = 'replace_regex_pattern';
            inp.title = mapTitle['replace_regex_pattern'];
            inp.placeholder = mapTitle['replace_regex_pattern'];
            input.replaceWith(inp);
        }
    });


    PushExpandAnkiRichButton('action-switch-text', '', (evt, fn) => {
        fn?.(evt);
        actionHelper.flushElementCache();
    });


    const actions = {
        // execute action
        dispatchAction(param, from = null, target = null) {
            this.handlers?.[param?.['operate-type']]?.action?.(param, from, target);
        },

    };
    const actionHelper = {

        executeAction(param, from = null, target = null) {
            from = from ? from : this.getFromEle(param);
            target = target ? target : this.getDestEle(param);
            actions.dispatchAction(param, from, target);
        },

        elementCache: {},

        getEleAndCache(field) {
            let el = this.elementCache[field];
            if (!el) {
                el = this.getFieldElement(field);
                this.elementCache[field] = el;
            }
            return el;
        },
        getFromEle(item, triggerEle = null) {
            const field = item['fetch-field'];
            if ('*' === field && triggerEle) {
                return triggerEle;
            }
            return this.getEleAndCache(field, triggerEle);
        },
        getDestEle(item) {
            const field = item?.['fetch-to-field'] ?? item['fetch-field'];
            return this.getEleAndCache(field);
        },

        getFieldElement(name) {
            let from = document.querySelector(`:where(.field-name)[value='${name}']`);
            return findParent(from, '.form-item,.sentence_setting')?.querySelector('.spell-content,.field-value') ?? null;
        },

        flushElementCache() {
            this.elementCache = {};
        },

        filterButton(isText) {
            return item => {
                const type = actions.handlers[item['operate-type']].scope;
                if (type !== 'all' && ((isText && type !== 'text')) || (!isText && type === 'text')) {
                    return false;
                }
                return item;
            }
        },

        isTextNode(ele) {
            return this.textNode.has(ele.nodeName)
        },

        switchAction(data = {}) {
            return e => {
                const v = e.target.value;
                findParent(e.target, '.fetch-item').querySelector('.fetch-action-container').replaceWith(actions.handlers[v].getTemplate(data));
            }
        },

        parseFetchRule(arr, rule = {}) {
            let valid = false;
            rule[''] = {};
            arr.forEach(item => {
                rule[item['super-fetch-name']] = item;
                if (!item['value-selector'] && !item['default-value'] && !item['fetch-format']) {
                    log('value-selector or default value emptied', item);
                    return;
                }
                valid = true;
                try {
                    rule[item['parent-super-name']]?.['children'] ?
                        rule[item['parent-super-name']]['children'].push(item)
                        : rule[item['parent-super-name']]['children'] = [item];
                } catch (e) {
                    console.log(item, 'error parent name', e);
                }

            });
            if (!valid) {
                return null;
            }

            return rule['']?.children ?? null;
        },

        buildFetchItem(data = {}) {
            data['operate-type'] = data['operate-type'] ?? 'fetch';
            const handler = actions.handlers[data['operate-type']];
            data['op'] = Object.keys(actions.handlers).map(k => [k, actions.handlers[k].text, {title: actions.handlers[k].desc}]);
            data['fetch-operator'] = handler.getTemplate(data);
            const div = templateHelper.buildTemplateHTML('fetch-base', data);
            div.querySelector('.operate-type').addEventListener('change', actionHelper.switchAction(data));
            div.querySelector('.fetch-active').addEventListener('change', fetchActive);
            return div;
        },
    };

    PushHookAnkiHtml(ankiContainer => {
        const div = templateHelper.buildTemplateHTML('fetch-form', {
            'sequentially-fetch': GM_getValue('sequentially-fetch', false),
        });
        div.className = 'form-item fetch-sentence-container';
        setting = div.querySelector('.select-setting');
        const ty = new Set(['add', 'remove']);
        setting.addEventListener('click', evt => {
            if (!evt.target.dataset?.['op'] || !ty.has(evt.target.dataset.op)) {
                return
            }
            if (evt.target.dataset.op === 'remove') {
                eventFn.remove(evt);
                return;
            }
            eventFn.add(evt);
        });
        inputEventSelectors.push('.super-fetch-name,.parent-super-name');
        setting.addEventListener('contextmenu', evt => evt.target.dataset.op === 'add' && eventFn.copy(evt));

        ankiContainer.querySelector('#autoSentenceField').parentElement.insertAdjacentElement('afterend', div);
    });


    return {
        ankiFetchClickFn,
        ankiFetchData,
        getAnkiFetchParams,
        arrayDiff: diff,
        setEleDrag,
        superFetchHook: {
            eventHook: eventFn, getVarVal,
            formProcessor, anchorFn: {
                p: el => el.parentElement,
                ns: el => el.nextElementSibling,
                ps: el => el.previousElementSibling,
            }, log,
            mapTitle, fetchActions: actions,
            fetchActionHelper: actionHelper,
            mergeMap: (obj, kv) => Object.keys(kv).forEach(k => obj[k] = kv[k]),
            hookLang: lang => Object.keys(lang).forEach(k => mapTitle[k] = lang[k]),
            lang: name => allowFn.lang(name),
            allowFn, htmlType, handleOp, opType, operations,
            buildChildrenHtmlFn: templateHelper,
        }
    }
})();