Bwiki Quick Editor

Wiki 快速编辑器,无需打开编辑页面。

// ==UserScript==
// @name         Bwiki Quick Editor
// @version      0.1
// @description  Wiki 快速编辑器,无需打开编辑页面。
// @author       lu
// @match        *://wiki.biligame.com/*
// @namespace    https://greasyfork.org/users/416853
// @require https://code.jquery.com/jquery-3.6.0.min.js
// @license CC BY-SA
// @license MIT license for highlight-within-textarea
// @license CC BY-SA for Bwiki Quick Editor
// ==/UserScript==

/*
 * highlight-within-textarea
 *
 * @author  Will Boyd
 * @github  https://github.com/lonekorean/highlight-within-textarea
 * @license MIT license
 *
 * this copy alreay a little modified by Lu.
 */

(function($) {
    let ID = 'hwt';

    let HighlightWithinTextarea = function($el, config) {
        this.init($el, config);
    };

    HighlightWithinTextarea.prototype = {
        init: function($el, config) {
            this.$el = $el;

            // backwards compatibility with v1 (deprecated)
            if (this.getType(config) === 'function') {
                config = { highlight: config };
            }

            if (this.getType(config) === 'custom') {
                this.highlight = config;
                this.generate();
            } else {
                console.error('valid config object not provided');
            }
        },

        // returns identifier strings that aren't necessarily "real" JavaScript types
        getType: function(instance) {
            let type = typeof instance;
            if (!instance) {
                return 'falsey';
            } else if (Array.isArray(instance)) {
                if (instance.length === 2 && typeof instance[0] === 'number' && typeof instance[1] === 'number') {
                    return 'range';
                } else {
                    return 'array';
                }
            } else if (type === 'object') {
                if (instance instanceof RegExp) {
                    return 'regexp';
                } else if (instance.hasOwnProperty('highlight')) {
                    return 'custom';
                }
            } else if (type === 'function' || type === 'string') {
                return type;
            }

            return 'other';
        },

        generate: function() {
            this.$el
                .addClass(ID + '-input ' + ID + '-content')
                .on('input.' + ID, this.handleInput.bind(this))
                .on('scroll.' + ID, this.handleScroll.bind(this));

            this.$highlights = $('<div>', { class: ID + '-highlights ' + ID + '-content' });

            this.$backdrop = $('<div>', { class: ID + '-backdrop' })
                .append(this.$highlights);

            this.$container = $('<div>', { class: ID + '-container' })
                .insertAfter(this.$el)
                .append(this.$backdrop, this.$el) // moves $el into $container
                .on('scroll', this.blockContainerScroll.bind(this));

            this.browser = this.detectBrowser();
            switch (this.browser) {
                case 'firefox':
                    this.fixFirefox();
                    break;
                case 'ios':
                    this.fixIOS();
                    break;
            }

            // plugin function checks this for success
            this.isGenerated = true;

            // trigger input event to highlight any existing input
            this.handleInput();
        },

        // browser sniffing sucks, but there are browser-specific quirks to handle
        // that are not a matter of feature detection
        detectBrowser: function() {
            let ua = window.navigator.userAgent.toLowerCase();
            if (ua.indexOf('firefox') !== -1) {
                return 'firefox';
            } else if (!!ua.match(/msie|trident\/7|edge/)) {
                return 'ie';
            } else if (!!ua.match(/ipad|iphone|ipod/) && ua.indexOf('windows phone') === -1) {
                // Windows Phone flags itself as "like iPhone", thus the extra check
                return 'ios';
            } else {
                return 'other';
            }
        },

        // Firefox doesn't show text that scrolls into the padding of a textarea, so
        // rearrange a couple box models to make highlights behave the same way
        fixFirefox: function() {
            // take padding and border pixels from highlights div
            let padding = this.$highlights.css([
                'padding-top', 'padding-right', 'padding-bottom', 'padding-left'
            ]);
            let border = this.$highlights.css([
                'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width'
            ]);
            this.$highlights.css({
                'padding': '0',
                'border-width': '0'
            });

            this.$backdrop
                .css({
                // give padding pixels to backdrop div
                'margin-top': '+=' + padding['padding-top'],
                'margin-right': '+=' + padding['padding-right'],
                'margin-bottom': '+=' + padding['padding-bottom'],
                'margin-left': '+=' + padding['padding-left'],
            })
                .css({
                // give border pixels to backdrop div
                'margin-top': '+=' + border['border-top-width'],
                'margin-right': '+=' + border['border-right-width'],
                'margin-bottom': '+=' + border['border-bottom-width'],
                'margin-left': '+=' + border['border-left-width'],
            });
        },

        // iOS adds 3px of (unremovable) padding to the left and right of a textarea,
        // so adjust highlights div to match
        fixIOS: function() {
            this.$highlights.css({
                'padding-left': '+=3px',
                'padding-right': '+=3px'
            });
        },

        handleInput: function() {
            let input = this.$el.val();
            let ranges = this.getRanges(input, this.highlight);
            let unstaggeredRanges = this.removeStaggeredRanges(ranges);
            let boundaries = this.getBoundaries(unstaggeredRanges);
            this.renderMarks(boundaries);

        },

        getRanges: function(input, highlight) {
            let type = this.getType(highlight);
            switch (type) {
                case 'array':
                    return this.getArrayRanges(input, highlight);
                case 'function':
                    return this.getFunctionRanges(input, highlight);
                case 'regexp':
                    return this.getRegExpRanges(input, highlight);
                case 'string':
                    return this.getStringRanges(input, highlight);
                case 'range':
                    return this.getRangeRanges(input, highlight);
                case 'custom':
                    return this.getCustomRanges(input, highlight);
                default:
                    if (!highlight) {
                        // do nothing for falsey values
                        return [];
                    } else {
                        console.error('unrecognized highlight type');
                    }
            }
        },

        getArrayRanges: function(input, arr) {
            let ranges = arr.map(this.getRanges.bind(this, input));
            return Array.prototype.concat.apply([], ranges);
        },

        getFunctionRanges: function(input, func) {
            return this.getRanges(input, func(input));
        },

        getRegExpRanges: function(input, regex) {
            let ranges = [];
            let match;
            while (match = regex.exec(input), match !== null) {
                ranges.push([match.index, match.index + match[0].length]);
                if (!regex.global) {
                    // non-global regexes do not increase lastIndex, causing an infinite loop,
                    // but we can just break manually after the first match
                    break;
                }
            }
            return ranges;
        },

        getStringRanges: function(input, str) {
            let ranges = [];
            let inputLower = input.toLowerCase();
            let strLower = str.toLowerCase();
            let index = 0;
            while (index = inputLower.indexOf(strLower, index), index !== -1) {
                ranges.push([index, index + strLower.length]);
                index += strLower.length;
            }
            return ranges;
        },

        getRangeRanges: function(input, range) {
            return [range];
        },

        getCustomRanges: function(input, custom) {
            let ranges = this.getRanges(input, custom.highlight);
            if (custom.className) {
                ranges.forEach(function(range) {
                    // persist class name as a property of the array
                    if (range.className) {
                        range.className = custom.className + ' ' + range.className;
                    } else {
                        range.className = custom.className;
                    }
                });
            }
            return ranges;
        },

        // prevent staggered overlaps (clean nesting is fine)
        removeStaggeredRanges: function(ranges) {
            let unstaggeredRanges = [];
            ranges.forEach(function(range) {
                let isStaggered = unstaggeredRanges.some(function(unstaggeredRange) {
                    let isStartInside = range[0] > unstaggeredRange[0] && range[0] < unstaggeredRange[1];
                    let isStopInside = range[1] > unstaggeredRange[0] && range[1] < unstaggeredRange[1];
                    return isStartInside !== isStopInside; // xor
                });
                if (!isStaggered) {
                    unstaggeredRanges.push(range);
                }
            });
            return unstaggeredRanges;
        },

        getBoundaries: function(ranges) {
            let boundaries = [];
            ranges.forEach(function(range) {
                boundaries.push({
                    type: 'start',
                    index: range[0],
                    className: range.className
                });
                boundaries.push({
                    type: 'stop',
                    index: range[1]
                });
            });

            this.sortBoundaries(boundaries);
            return boundaries;
        },

        sortBoundaries: function(boundaries) {
            // backwards sort (since marks are inserted right to left)
            boundaries.sort(function(a, b) {
                if (a.index !== b.index) {
                    return b.index - a.index;
                } else if (a.type === 'stop' && b.type === 'start') {
                    return 1;
                } else if (a.type === 'start' && b.type === 'stop') {
                    return -1;
                } else {
                    return 0;
                }
            });
        },

        renderMarks: function(boundaries) {
            let input = this.$el.val();
            boundaries.forEach(function(boundary, index) {
                let markup;
                if (boundary.type === 'start') {
                    markup = '{{hwt-mark-start|' + index + '}}';
                } else {
                    markup = '{{hwt-mark-stop}}';
                }
                input = input.slice(0, boundary.index) + markup + input.slice(boundary.index);
            });

            // this keeps scrolling aligned when input ends with a newline
            input = input.replace(/\n(\{\{hwt-mark-stop\}\})?$/, '\n\n$1');

            // encode HTML entities
            input = input.replace(/</g, '&lt;').replace(/>/g, '&gt;');

            if (this.browser === 'ie') {
                // IE/Edge wraps whitespace differently in a div vs textarea, this fixes it
                input = input.replace(/ /g, ' <wbr>');
            }

            // replace start tokens with opening <mark> tags with class name
            input = input.replace(/\{\{hwt-mark-start\|(\d+)\}\}/g, function(match, submatch) {
                var className = boundaries[+submatch].className;
                if (className) {
                    return '<span class="' + className + '">';
                } else {
                    return '<span>';
                }
            });

            // replace stop tokens with closing </mark> tags
            input = input.replace(/\{\{hwt-mark-stop\}\}/g, '</span>');

            this.$highlights.html(input);
        },

        handleScroll: function() {
            let scrollTop = this.$el.scrollTop();
            this.$backdrop.scrollTop(scrollTop);

            // Chrome and Safari won't break long strings of spaces, which can cause
            // horizontal scrolling, this compensates by shifting highlights by the
            // horizontally scrolled amount to keep things aligned
            let scrollLeft = this.$el.scrollLeft();
            this.$backdrop.css('transform', (scrollLeft > 0) ? 'translateX(' + -scrollLeft + 'px)' : '');
        },

        // in Chrome, page up/down in the textarea will shift stuff within the
        // container (despite the CSS), this immediately reverts the shift
        blockContainerScroll: function() {
            this.$container.scrollLeft(0);
        },

        destroy: function() {
            this.$backdrop.remove();
            this.$el
                .unwrap()
                .removeClass(ID + '-text ' + ID + '-input')
                .off(ID)
                .removeData(ID);
        },
    };

    // register the jQuery plugin
    $.fn.highlightWithinTextarea = function(options) {
        return this.each(function() {
            let $this = $(this);
            let plugin = $this.data(ID);

            if (typeof options === 'string') {
                if (plugin) {
                    switch (options) {
                        case 'update':
                            plugin.handleInput();
                            break;
                        case 'destroy':
                            plugin.destroy();
                            break;
                        default:
                            console.error('unrecognized method string');
                    }
                } else {
                    console.error('plugin must be instantiated first');
                }
            } else {
                if (plugin) {
                    plugin.destroy();
                }
                plugin = new HighlightWithinTextarea($this, options);
                if (plugin.isGenerated) {
                    $this.data(ID, plugin);
                }
            }
        });
    };
})(jQuery);

function hwt_add_css(){

    let s = document.createElement("style");
    s.innerHTML = `
.hwt-container {
	display: inline-block;
	position: relative;
	overflow: hidden !important;
	-webkit-text-size-adjust: none !important;
}

/* z-index is support for bwiki */
.hwt-backdrop {
	position: absolute !important;
	top: 0 !important;
	right: -99px !important;
	bottom: 0 !important;
	left: 0 !important;
	padding-right: 99px !important;
	overflow-x: hidden !important;
	overflow-y: auto !important;
    z-index: 1501;
    pointer-events: none;
    font-family: Consolas;
    color: black;
}

.hwt-highlights {
	width: auto !important;
	height: auto !important;
	border-color: transparent !important;
	white-space: pre-wrap !important;
	word-wrap: break-word !important;
	overflow: hidden !important;
}

.hwt-input {
	display: block !important;
	position: relative !important;
	margin: 0;
	padding: 0;
	border-radius: 0;
	font: inherit;
	overflow-x: hidden !important;
	overflow-y: auto !important;
}

.hwt-content {
	border: 1px solid;
	background: none transparent !important;
}

.hwt-content span {
	padding: 0 !important;
}
`;
    document.head.appendChild(s);
}

hwt_add_css()

/*
 * END OF
 * highlight-within-textarea
 *
 * @author  Will Boyd
 * @github  https://github.com/lonekorean/highlight-within-textarea
 * @license MIT license
 *
 * this copy alreay a little modified by Lu.
 */


/*
 * Bwiki Quick Editor
 * @author  Lu
 * @bid 39886146
 * @license CC BY-SA
 *
 * TODO: 检查mediawiki命名空间中的文件后缀,增加CSS/JS高亮规则
 *
 */

const edit_btn_id = "lu_edit_btn";
const pannel_id = "lu_editor";
const pannel_textarea_id = "lu_editor_textarea";

function main(){
    // 检查是否适合编辑
    if (!editable()) {
        console.log("Quick Editor: 本页面暂不支持使用。因为mw属性中 不是article 或者 action 不为 view");
        return;
    }

    add_editor_hightlight_css(); // 添加 css
    init_editor_wiki_code(); // 初始化编辑器文本
    addEditBtn(); // 添加右下角按钮
    addEditorPannel(); // 添加编辑面板
    addClickByID(edit_btn_id, edit_btn_onclick); // 按钮事件
    addClickByID("lu_edit_close", edit_btn_onclick); // 关闭按钮事件
    addClickByID("lu_edit_save", save_btn_onclick); // 保存按钮事件

}

function add_editor_hightlight_css(){
    let diy_style = document.createElement("style");
    diy_style.innerHTML = `
        .hwt-content .code_brackets {color:#6B00A8;}
        .hwt-content .code_tag {color:#0071B6;}
        .hwt-content .code_link {color:#0071B6;}
        .hwt-content .code_tag_attr {color:#00B2CB;}
        .hwt-content .code_template {color:#224CB0;}
        .hwt-content .code_magic_words {color: #00B2CB;}
        .hwt-content .code_template_name {color:#8F27A4;}
        .hwt-content .code_var {color:#0033bb;}
        .hwt-content .code_var .code_template_name {color:#0033bb; font-weight: bold;}
        .hwt-content .code_head {color: #0094C2; font-weight: bold;}
        .hwt-content .code_head_text {color: #0064B2; font-weight: bold;}
        .hwt-content .code_comment {color:green; opacity: 0.55; font-style:italic;}

        .hwt-content .code_italic {font-style:italic;}
        .hwt-content .code_bold {font-weight: bold;}
        .hwt-content .code_strike {text-decoration: line-through;}
    `;
    document.head.appendChild(diy_style);
}

function init_editor_wiki_code() {
    // 拼接API(获取当前页面 wiki 代码)
    let api = get_api_url();
    const page_id = get_page_id();
    let page_code_url = api + "?action=query&format=json&prop=revisions&rvlimit=1" +
        "&rvprop=ids|timestamp|flags|comment|user|userid|content&pageids=" + page_id.toString();

    // 获取本页内容
    fetch(page_code_url).then(response => response.json()).then(data => {
        document.getElementById(pannel_textarea_id).value = data.query["pages"][page_id]["revisions"][0]["*"];
        // 代码高亮 初始化
        highlight_wiki_text();
    });
}

function editable(){
    /* 检查此页面是否可以编辑
    * 条件: wgIsArticle == true 和 wgAction == view
    * */
    let is_article = mw.config.get("wgIsArticle")
    let page_action = mw.config.get("wgAction")
    if (!(is_article && page_action === "view")) {
        console.log("Quick Editor:  not article or not view action, editor will not load");
        return false;
    }
    return true;
}

function get_api_url(){ /* 拼接 bwiki api url */
    return "https://wiki.biligame.com" + mw.config.get("wgScriptPath") + "/api.php";
}
function get_page_id(){ /* 获取 page id */
    return mw.config.get("wgArticleId").toString();
}

function save_btn_onclick(){
    /* 保存 */
    document.getElementById("lu_edit_save").disabled=true;
    setTimeout(function (){
        document.getElementById("lu_edit_save").disabled=false;
    }, 5000);

    // 获取需要的信息
    let wikitext = document.getElementById(pannel_textarea_id).value;
    let summary = document.getElementById("lu_edit_comment").value;
    let page_id = get_page_id();
    let api = get_api_url();

    /* 先获取token,然后保存 */
    fetch(api+"?meta=tokens&action=query&format=json")
    .then(function(res){return res.json()})
    .then(function(res_data){
        let token = res_data.query.tokens["csrftoken"];

        // 保存需要的参数
        let param = new FormData();
        param.append("action","edit");
        param.append("format","json");
        param.append("token", token);
        param.append("pageid", page_id);
        param.append("text", wikitext);
        param.append("summary", summary);
        param.append("minor",false);
        param.append("nocreate", true);

        // 保存
        fetch(api, {
            method: "POST",
            body: param
        }).then(function(res){console.log(res);return res.json()}).then(function(data){
            // 返回的json有至少三种可能:
            // {"edit":{"result":"Success","pageid":123,"title":"...","oldrevid":2310,"newrevid":2313, ...}}
            // {"edit":{"result":"Success","pageid":123,"title":"...","nochange":"",...}}
            // {"error":{"code":"articleexists","info":"The article you tried to create has been created already."...}}
            console.log(JSON.stringify(data));
            if(data.error){
                alert(error.code + "     " + error.info);
                return;
            }
            if(data.edit.result){
                if(data.edit["nochange"] === ""){
                    alert("成功提交,但你没有修改任何内容");
                }else{
                    alert("成功保存更改");
                }
                location.reload();
            }
        });
    });
}

function highlight_wiki_text_update(){ /* 手动更新高亮 */
   $('#'+pannel_textarea_id).highlightWithinTextarea('update');
}

/* 高亮 mediawiki code
 * 基于正则匹配关键词
 * 参考了
 * - https://github.com/ajaxorg/ace/blob/23208f2f19/src/mode/mediawiki_highlight_rules.js
 * - https://github.com/Frederisk/Wikitext-VSCode-Extension/blob/master/language-configuration.yaml
*/
function highlight_wiki_text(){
    return $('#' + pannel_textarea_id).highlightWithinTextarea({
        highlight: [
            { // 注释
                highlight: /<!--[^]*?-->/gi,
                className: 'code_comment'
            },
            { // 括号 和 特定符号
                highlight: ["{{{", "}}}", "{{", "}}", "|", "[[", "]]", "[", "]", "----"],
                className: 'code_brackets'
            },
            { // 括号 和 特定符号,如表格。最后一项是匹配行首的“|”
                highlight: ["||", "!!", "-|", "|-", "-{", "}-", "{|", "|}", /(?<=\n|^)\|/gi],
                className: 'code_brackets code_bold'
            },
            { // 支持:行首的叹号,允许前有空格; 行首的*#:
                highlight: [/(?<=\n[\s]*)!/gi, /(?<=\n|^)[*#:;]+/gi],
                className: 'code_brackets code_bold'
            },
            { // 匹配链接, [balabala],[[balabala]] [url balabala]
                highlight: [/(?<=\[)[^\]\|\n\s]+(?=[(\]\])|\||\s])/gi, /(?<=\[\[)[^\]\|\n]+(?=[(\]\])|\|])/gi, /[a-zA-z]+:\/\/[^\s]*/gi],
                className: 'code_link'
            },
            { // 模板名字,以 {{ 开始,遇到{、}、|、换行停止
                highlight: /(?<={{)[^{}\|\n]+/gi,
                className: 'code_template_name'
            },
            { // 变量。 {{{xxx}}}
                highlight: /{{{[^}]+}}}/gi,
                className: 'code_var'
            },
            { // 斜体 '''it'''
                highlight: [/'''[^'\n]+'''/gi],
                className: 'code_italic'
            },
            { // 粗体 ''it'' 第二个正则匹配前后可能有斜体的粗体
                highlight: [/(?<=[^']+)''[^'\n]+''/gi, /(?<=('''[^'\n]+'''))''[^'\n]+''(?=([^']|('''[^'\n]+''')))/gi],
                className: 'code_bold'
            },
            { // 五个点代表粗体+斜体
                highlight: [/'''''[^'\n]+'''''/gi],
                className: 'code_bold code_italic'
            },
            { // 标题内容 === 只匹配中间部分 ===
                highlight: /(?<=(\n|^)[=]+)[^=\n]+(?==)/gi,
                className: 'code_head_text'
            },
            { // 标题 == head ==
                highlight: /(?<=\n|^)(={1,6})([^=\n]+)(\1)(?!=)/gi,
                className: 'code_head'
            },
            { // 重定向
                highlight: /(#REDIRECT|#redirect|#Redirect|#重定向)(\s+)/gi,
                className: 'code_magic_words'
            },
            { // magic words
                highlight: /(__NOTOC__|__FORCETOC__|__TOC__|__NOEDITSECTION__|__NEWSECTIONLINK__|__NONEWSECTIONLINK__|__NOWYSIWYG__|__NOGALLERY__|__HIDDENCAT__|__EXPECTUNUSEDCATEGORY__|__NOCONTENTCONVERT__|__NOCC__|__NOTITLECONVERT__|__NOTC__|__START__|__END__|__INDEX__|__NOINDEX__|__STATICREDIRECT__|__NOGLOBAL__|__DISAMBIG__)/gi,
                className: 'code_magic_words'
            },
            { // tag,类HTML标签
                highlight: /<[/]?[^>\n!]+>/gi,
                className: 'code_tag'
            },
            { // 标签属性 tag attr , 类HTML标签中,第一个空格后的部分
                highlight: /(?<=<[/]?[^>\n!]+)\s[^>]+/gi,
                className: 'code_tag_attr'
            },
            { // for <s>
                highlight: /(?<=<s>)[^<]*(?=<\/s>)/gi,
                className: 'code_strike'
            }

        ]
    });
}

function edit_btn_onclick(){
    /* 点击编辑按钮后显示/隐藏编辑框。但要判定窗口够不够大 */
    let btn = document.getElementById(pannel_id);
    if(btn.style.display === 'block'){
        btn.style.display = 'none';
    }else{
        if(ScreenBigEnough()){
            alert("Editor 需要屏幕大于800x500像素才能正常显示");
            return;
        }
        btn.style.display = 'block';
    }
}

function ScreenBigEnough(){
    return (window.innerHeight < 500)|| (window.innerWidth < 800);
}

function addClickByID(elm_id, func) { /* 为ID元素增加点击事件 */
    document.getElementById(elm_id).addEventListener("click", func);
}

function addEditorPannel(){
    /* 添加编辑栏 */

    // 面板框架
    let pannel = document.createElement("div");
    pannel.id = pannel_id;
    pannel.innerHTML = `<div style="font-weight: bold;">快速编辑器(测试版)</div><br>`;
    /* fix定位的编辑框 其中z-index针对 */
    pannel.style = `
        display:none;
        position: fixed;
        width: 800px;
        height: 600px;
        top: 10%;
        right: 10%;
        padding:10px;
        background: white;
        border:1px solid rgba(172,172,172,0.6);
        font-size:1.2em;
        z-index: 1501;
    }`;


    // 编辑框
    let textarea = document.createElement("textarea");
    textarea.id = pannel_textarea_id;
    textarea.style = `
        width: 780px;
        height: 400px;
        font-family: Consolas;
        outline:none;
        border:1px solid rgba(172,172,172,0.4);
        color: white;
        color: rgba(0,0,0,0);
        caret-color: black;
    }`;
    textarea.innerHTML = "尝试加载本页代码中…… (如果读完这句话还没加载完,大概率是脚本出Bug了)"
    pannel.appendChild(textarea);

    let the_div = document.createElement("div");
    the_div.innerHTML = `
        <div style="margin:10px 0;">编辑摘要: <!--<small>快速选择:</small>-->
            <input id="lu_edit_comment" name="lu_edit_comment" style="width:100%;" type="text" value="" placeholder="请简要描述您所作出的修改"></input>
        </div>
        <button id="lu_edit_save" class="btn btn-success">保存</button>
        <div id="lu_edit_close" style="float:right;" class="btn btn-warning">关闭</div>
    `;
    pannel.appendChild(the_div);

    document.body.append(pannel);
}


function addEditBtn() {
    /* 右下角增加编辑按钮, 基于bootstrap btn 样式 */
    let edit_btn = document.createElement("div");
    edit_btn.innerHTML = "快速编辑";
    edit_btn.style = "position: fixed; bottom: 5px; right: 5px; z-index: 1500;";
    edit_btn.id = edit_btn_id;
    edit_btn.classList.add("btn");
    edit_btn.classList.add("btn-info");
    document.body.append(edit_btn);
    return edit_btn;
}


// 入口
setTimeout(function (){
    main();
}, 1000); // timeout 是为了等待 mw 全部加载。