ankienhance

anki extract sentence enhance

このスクリプトは単体で利用できません。右のようなメタデータを含むスクリプトから、ライブラリとして読み込まれます: // @require https://update.greatest.deepsurf.us/scripts/534396/1834624/ankienhance.js

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
;const {ankiFetchClickFn, 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 = {
        cacheSwitchData(ev) {
            if ('operate-type' !== ev.target.className) {
                return
            }
            const o = actionHelper.initSwitch(ev.target);
            o[ev.target.value] = actionHelper.getSwitchData(ev.target);
        },
        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 = async (ev) => {
                if (sel.value) {
                    await 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);
        },
        selector: '.fetch-import,.fetch-export',
        showProcessor(ev) {
            if (!ev.target.checked) {
                actionHelper.switchData = new WeakMap();
                saveFetchItems();
                freshBtns();
                setting.children[0].classList.add('fetch-hidden');
                getFetchItemEles().map(e => e.remove());
                ev.target.parentElement.querySelectorAll(eventFn.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'));
            }
            ev.target.parentElement.querySelectorAll(eventFn.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, ev) => eventFn.addTplFn?.[fn]({}, ev),

        },

        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], ev);
                    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', '', async (ev) => {
        await 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);
    });

    PushHookAnkiChange('.operate-type', ev => actionHelper.switchAction(ev));

    // 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 ? [...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') {
                    v = Number(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 = {
        'log': '打印到控制台',
        'no file': '没有文件!',
        'handleValue': '对值进行处理',
        'fold-or-unfold': '折叠或展开子项',
        'handleElement': '处理元素',
        'handleElement-desc': '只作用于富文本字段,处理指定选择器对应的元素',
        'childUseIndependentSymbol': '所有子项使用独立的符号表',
        '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]...}',
        'handleType': '替换目标类型',
        'text': '文本',
        'add': '左键添加一个空白项,右键复制当前项',
        'innerHTML': 'innerHTML',
        'outerHTML': 'outerHTML',
        'searchValue': '替换或删除的目标,文本值或选择器',
        'replaceValue': '替换的值,当为删除时值为选择器',
        'remove element': '删除元素',
        'pattern': '正则替换模式如果是正则替换的化,为空则为普通替换',
        'cover': '覆盖',
        'append': '追加',
        'none': '啥都不干',
        'htmlElement': '元素',
        'add-contextmenu': '加入到Tampermonkey菜单',
        'url-scope': '生效作用域,正则匹配url,多个用||隔开,只对自动执行或添加tampermonkey菜单有效',
    };


    async function executeActions(...names) {
        let vars = {}, async = true;
        names = names.filterAndMapX(name => {
            if ('string' === typeof name && name) {
                return name
            } else if ('object' === typeof name) {
                vars = name;
            } else if ('boolean' === typeof name) {
                async = name;
            }
            return false
        });
        if (names.length < 1) {
            return;
        }
        const rules = {};
        getAnkiFetchParams().forEach(rule => rules[rule['fetch-name']] = rule);
        for (const name of names) {
            try {
                if (rules?.[name]) {
                    if (async) {
                        actionHelper.executeAction(rules[name], null, null, null, vars);
                    } else {
                        await actionHelper.executeAction(rules[name], null, null, null, vars);
                    }
                }
            } catch (e) {
                console.log('execute action', name, 'error:', e);
            }
        }
    }

    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;
        });
    }

    async function ankiFetchClickFn(button) {
        const triggerField = button.parentElement.parentElement.querySelector('.field-name').value.trim();
        const param = getAnkiFetchParams(triggerField, true);
        if (param.length < 1) {
            return;
        }
        const triggerEle = button.parentElement.previousElementSibling.querySelector('.spell-content,.field-value');
        const sequence = GM_getValue('sequentially-fetch', false);
        if (!sequence) {
            for (const item of param) {
                await actionHelper.executeAction(item, null, null, triggerEle);
            }
            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']);
        for (const child of from.children) {
            for (const item of fetchItems) {
                await actionHelper.executeAction(item, child, null, triggerEle)
            }
        }
    }

    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);
    }

    const varReg = /\{.*}/, varRegs = /^\{.*?}$/;

    function getVariable(vars, express, defaults = '', bracket = false) {
        if (bracket && !varReg.test(express)) {
            return express;
        }
        if (varRegs.test(express)) {
            express = trims(express, '{}');
        }
        return getVarVal(vars, express, defaults);
    }

    function getVarVal(vars, express, defaults = '') {
        if (vars?.[express]) {
            return vars[express];
        }
        if (!express.includes('.')) {
            return vars?.[express] ?? defaults;
        }
        express = express.replace(actionHelper.fetchReplaceVarsRex, (substring, args) => getVarVal(vars, args, undefined));
        if (!express) {
            return 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'],
        'log': mapTitle['log'],
        'none': mapTitle['none']
    };
    const operations = {fetch: mapTitle['fetch'], handle: mapTitle['handle']};
    const 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);
            if ('string' === typeof attrs) {
                el.innerHTML = attrs;
            } else {
                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;
                if (names.length < 2) {
                    const v = names[0].split('?');
                    return v.length > 1 ? getVarVal(vars, v[0], v[1]) : getVarVal(vars, v[0]);
                } else {
                    val = getVarVal(vars, names[0]);
                }
                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];
        },

        buildFormElement: {
            input(name, value, attr = {}) {
                attr.title = attr?.title ?? mapTitle[`${name}-desc`] ?? mapTitle[name] ?? name;
                attr.placeholder = attr.title;
                const input = templateHelper.createElement('input', {name, className: name, ...attr});
                'text' === attr.type && (value = htmlSpecial(value));
                'number' === attr.type && (value = value ? value : 0);
                input.value = value;
                return input
            },
            select(name, options, attr = {}) {
                attr.title = attr?.title ?? mapTitle[`${name}-desc`] ?? mapTitle[name] ?? name;
                return templateHelper.createElement('select', {
                    name, className: name,
                    innerHTML: options,
                    ...attr
                });
            },
            textarea(name, value, attr = {}) {
                attr.placeholder = attr?.placeholder ?? mapTitle[`${name}-desc`] ?? mapTitle[name] ?? name;
                return templateHelper.createElement('textarea', {name, className: name, value, ...attr})
            }
        }
    };

    function setEleDrag(ele, selector, parentSelector = '', config = {}) {
        const turnDrag = onoff => ele.querySelectorAll(selector).forEach(item => item.draggable = onoff);
        const param = {
            currentMovingEle: null,
            flag: false, parent: null, turnDrag,
            target: null
        };
        const evenFn = {
            dragstart(e) {
                if (!e.target.matches(selector)) {
                    return
                }
                e.stopImmediatePropagation();
                e.dataTransfer.effectAllowed = 'move';
                param.currentMovingEle = e.target;
                param.currentMovingEle.classList.add('moving');
                if (parentSelector) {
                    param.parent = findParent(param.currentMovingEle, parentSelector);
                }
            },
            dragenter(e) {
                e.preventDefault();
                if (!param.currentMovingEle) {
                    return
                }
                const children = [...(param.parent ? param.parent : ele).querySelectorAll(selector)];
                if (e.target === param.currentMovingEle || children.length <= 1) {
                    return
                }
                if (!e.target.classList.contains('moving') && !param.currentMovingEle?.classList?.contains('moving')) {
                    return;
                }
                const curIndex = children.indexOf(param.currentMovingEle);
                if (curIndex < 0) {
                    //log('not '+selector,e.target)
                    return;
                }
                let tarIndex = children.indexOf(e.target);
                let tarEle = e.target;
                if (tarIndex < 0) {
                    tarEle = findParent(e.target, selector);
                    if (!tarEle) {
                        return;
                    }
                    tarIndex = children.indexOf(tarEle);
                    if (tarIndex < 0 || tarIndex === curIndex || tarEle.classList.contains('moving')) {
                        return;
                    }
                    if (param.target === tarEle) {
                        return;
                    }
                }
                param.target = tarEle;
                tarEle.insertAdjacentElement(tarIndex > curIndex ? 'afterend' : 'beforebegin', param.currentMovingEle);

            },
            dragend(e) {
                e.preventDefault();
                if (!e.target.matches(selector)) {
                    return
                }
                e.stopPropagation();
                param.target = null;
                param.currentMovingEle && param.currentMovingEle.classList.remove('moving');
                param.currentMovingEle = null;
            },
            dragover(e) {
                !actionHelper.isTextNode(e.target) && e.preventDefault();
                if (!e.target.matches(selector)) {
                    return
                }
                e.stopPropagation();
            },
            mousedown(ev) {
                if (actionHelper.isTextNode(ev.target)) {
                    turnDrag(false);
                    param.flag = true
                }
            },
            mouseup() {
                if (param.flag) {
                    turnDrag(true);
                    param.flag = false;
                }
            },
            ...config
        }
        turnDrag(true);
        Object.keys(evenFn).forEach(name => ele.addEventListener(name, evt => evenFn[name](evt, param)));
    }


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


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

    };
    const actionHelper = {

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

        elementCache: {},

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

        getFieldElement(field) {
            if ('$doc' === field) {
                return document
            }
            const element = document.querySelector(`.field-name[value='${field}']`);
            if (!element) {
                return null;
            }
            return findParent(element, '.form-item')?.querySelector('.spell-content,.field-value');
        },

        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)
        },

        initSwitch(select) {
            if (!actionHelper.switchData.has(select)) {
                actionHelper.switchData.set(select, {});
            }
            return actionHelper.switchData.get(select);
        },

        getSwitchData(select) {
            return formProcessor.convertFetchParam(findParent(select, '.fetch-item'))
        },

        switchAction(e) {
            const o = this.initSwitch(e.target);
            const v = e.target.value;
            const data = o?.[v] ?? {};
            o[v] = this.getSwitchData(e.target);
            const fetchItem = findParent(e.target, '.fetch-item');
            fetchItem.querySelectorAll('[data-single-run]').forEach(el => el.dataset.singleRun = actions.handlers[v]?.singleRun ?? false);
            const container = fetchItem.querySelector('.fetch-action-container');
            container.replaceWith(actions.handlers[v].getTemplate(data));
        },

        parseFetchRule(arr, rule = {}) {
            let valid = false;
            rule[''] = {};
            arr.forEach(item => {
                if (!item['super-fetch-name']) {
                    log('name can not be empty', item);
                    return;
                }
                rule[item['super-fetch-name']] = item;
                if (!item['value-selector'] && !item['default-value'] && !item['fetch-format'] && !item?.handleValue) {
                    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;
        },

        switchData: new WeakMap(),

        buildFetchItem(data = {}) {
            data['operate-type'] = data['operate-type'] ?? 'fetch';
            data.singleRun = actions.handlers[data['operate-type']]?.singleRun ?? false;
            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);
            return templateHelper.buildTemplateHTML('fetch-base', data);
        },
    };

    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);
        });
        ankiContainer.addEventListener('mousedown', ev => eventFn.cacheSwitchData(ev));
        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,
        getAnkiFetchParams,
        arrayDiff: diff,
        setEleDrag,
        superFetchHook: {
            executeActions,
            eventHook: eventFn, getVarVal, getVariable,
            formProcessor, anchorFn: {
                p: el => el.parentElement,
                ns: el => el.nextElementSibling,
                ps: el => el.previousElementSibling,
            }, log,
            mapTitle, fetchActions: actions,
            fetchActionHelper: actionHelper,
            mergeMap: (obj, newObj) => iterateObjByKey(newObj, (k, v) => obj[k] = v, false),
            hookLang: langKV => iterateObjByKey(langKV, (k, v) => mapTitle[k] = htmlSpecial(v), false),
            lang: name => allowFn.lang(name) ?? name,
            allowFn, htmlType, handleOp, operations,
            templateHelper,
        }
    }
})();