コンテンツにスキップ

利用者:Dragoniez/scripts/dragoLib.js

これはこのページの過去の版です。Dragoniez (会話 | 投稿記録) による 2022年11月6日 (日) 13:09個人設定で未設定ならUTC)時点の版 (add note)であり、現在の版とは大きく異なる場合があります。

お知らせ: 保存した後、ブラウザのキャッシュをクリアしてページを再読み込みする必要があります。

多くの WindowsLinux のブラウザ

  • Ctrl を押しながら F5 を押す。

Mac における Safari

  • Shift を押しながら、更新ボタン をクリックする。

Mac における ChromeFirefox

  • Cmd Shift を押しながら R を押す。

詳細についてはWikipedia:キャッシュを消すをご覧ください。

/****************************************
 * dragoLib: Versatile function library *
 ****************************************/
//<nowiki>

if (typeof dragoLib === 'undefined') {

// ****************************************************************

var dragoLib = {

// ******************************** SYNCHRONOUS FUNCTIONS ********************************

/**
 * Get strings enclosed by <!-- -->, <nowiki />, <pre />, <syntaxhighlight />, and <source />
 * @param {string} wikitext 
 * @returns {Array|null} 
 */
extractCommentOuts: function(wikitext) {
    return wikitext.match(/(<!--[\s\S]*?-->|<nowiki>[\s\S]*?<\/nowiki>|<pre[\s\S]*?<\/pre>|<syntaxhighlight[\s\S]*?<\/syntaxhighlight>|<source[\s\S]*?<\/source>)/gm);
},

/** 
 * Extract templates from wikitext
 * @requires mediawiki.util
 * @param {string} wikitext the wikitext from which templates are extracted
 * @param {string|Array} [templateName] sort out templates by these template names
 * @param {string|Array} [templatePrefix] sort out templates by these template prefixes
 * @returns {Array}
 */
findTemplates: function(wikitext, templateName, templatePrefix) {
    if (paramsMissing(arguments, 1)) throwError('findTemplates');

    // Create an array by splitting the original content with '{{'
    var tempInnerContent = wikitext.split('{{'); // Note: tempInnerContent[0] is always an empty string or a string that has nothing to do with templates
    if (tempInnerContent.length === 0) return [];

    // Extract templates from the wikitext
    var templates = [], nest = [];
    for (var i = 1; i < tempInnerContent.length; i++) { // Loop through all elements in tempInnerContent (except tempInnerContent[0])

        (function() { // Create a block scope of 'for'

            var tempTailCnt = (tempInnerContent[i].match(/\}\}/g) || []).length; // The number of '}}' in the split array
            var temp = ''; // Temporary escape hatch

            // There's no '}}' (= nesting other templates)
            if (tempTailCnt === 0) {

                nest.push(i); // Save the index of the element in the array

            // There's one '}}' in the element of the array (= the left part of '}}' is the whole of the template's parameters)
            } else if (tempTailCnt === 1) {

                temp = '{{' + tempInnerContent[i].split('}}')[0] + '}}';
                if ($.inArray(temp, templates) === -1) templates.push(temp);

            // There're two or more '}}'s (e.g. 'TL2|...}}...}}'; = templates are nested)
            } else {

                for (var j = 0; j < tempTailCnt; j++) {

                    if (j === 0) { // The innermost template

                        temp = '{{' + tempInnerContent[i].split('}}')[j] + '}}'; // Same as when there's one '}}' in the element
                        if ($.inArray(temp, templates) === -1) templates.push(temp);

                    } else { // Multi-nested template(s)

                        var elNum = nest[nest.length -1]; // The index of the element that involves the start of the nest
                        nest.pop(); // The index won't be reused after reference
                        var nestedTempInnerContent = tempInnerContent[i].split('}}'); // Create another array by splitting with '}}'

                        temp = '{{' + tempInnerContent.slice(elNum, i).join('{{') + '{{' + nestedTempInnerContent.slice(0, j + 1).join('}}') + '}}';
                        if ($.inArray(temp, templates) === -1) templates.push(temp);

                    }

                }

            }

        })();

    } // All templates in the wikitext is stored in 'templates' when the loop is done

    // Remove templates that are part of comments
    var co = dragoLib.extractCommentOuts(wikitext);
    if (co) {
        co.forEach(function(item) {
            templates = templates.filter(function(template) {
                return item.indexOf(template) === -1;
            });
        });
    }

    // End here if the templates don't need to be sorted
    if ((!templateName && !templatePrefix) || templates.length === 0) return templates;

    // Convert passed parameters to arrays if they are strings
    if (templateName && typeof templateName === 'string') templateName = [templateName];
    if (templatePrefix && typeof templatePrefix === 'string') templatePrefix = [templatePrefix];

    /**
     * Function to create a regex that makes the first character of a template case-insensitive
     * @param {string} str 
     * @returns {string} [Xx]
     */
    var caseInsensitiveFirstLetter = function(str) {
        return '[' + str.substring(0, 1).toUpperCase() + str.substring(0, 1).toLowerCase() + ']';
    };

    // Create regex for template sorting
    var names = [], prefixes = [];
    if (templateName) {
        for (var i = 0; i < templateName.length; i++) {
            names.push(caseInsensitiveFirstLetter(templateName[i]) + mw.util.escapeRegExp(templateName[i].substring(1)));
        }
        var templateNameRegExp = new RegExp('^(' + names.join('|') + ')$');
    }
    if (templatePrefix) {
        for (var i = 0; i < templatePrefix.length; i++) {
            prefixes.push(caseInsensitiveFirstLetter(templatePrefix[i]) + mw.util.escapeRegExp(templatePrefix[i].substring(1)));
        }
        var templatePrefixRegExp = new RegExp('^(' + prefixes.join('|') + ')');
    }

    // Sort out certain templates
    templates = templates.filter(function(item) {
        var name = item.match(/^\{{2}\s*([^\|\{\}\n]+)/); // {{ TEMPLATENAME | ... }} の TEMPLATENAME を抽出
        if (!name) return;
        name = name[1].trim();
        if (templateName && templatePrefix) {
            return name.match(templateNameRegExp) || name.match(templatePrefixRegExp);
        } else if (templateName) {
            return name.match(templateNameRegExp);
        } else if (templatePrefix) {
            return name.match(templatePrefixRegExp);
        }
    });

    return templates;

},

/**
 * Get the parameters of templates as an array
 * @param {string} template 
 * @returns {Array} This doesn't contain the template's name
 */
getTemplateParams: function(template) {
    if (paramsMissing(arguments, 1)) throwError(getTemplateParams);

    // If the template doesn't contain '|', it doesn't have params
    if (template.indexOf('|') === -1) return [];

    // Remove the first '{{' and the last '}}' (or '|}}')
    var frameRegExp = /(?:^\{{2}|\|*\}{2}$)/g;
    var params = template.replace(frameRegExp, '');

    // In case the params nest other templates
    var nested = dragoLib.findTemplates(params);
    if (nested.length !== 0) {
        // Sort out templates that don't nest yet other templates (findTemplates() returns both TL1 and TL2 in {{TL1| {{TL2}} }} ), but we don't need TL2
        nested = nested.filter(function(item) {
            return nested.filter(function(itemN) { return itemN !== item; }).every(function(itemN) { return itemN.indexOf(item) === -1; });
            // ↳ Look at the other elements in the array 'nested' (.filter) and only preserve items that are not part of those items (.every)
        });
        nested.forEach(function(item, i) {
            params = params.split(item).join('$TL' + i); // Replace nested templates with '$TLn'
        });
    }

    // Get an array of parameters
    params = params.split('|'); // This could be messed up if the nested templates hadn't been replaced
    params.shift(); // Remove the template name
    if (nested.length !== 0) {
        params.forEach(function(item, i) {
            var m;
            if (m = item.match(/\$TL\d+/g)) { // Find all $TLn in the item
                for (var j = 0; j < m.length; j += 2) {
                    var index = m[j].match(/\$TL(\d+)/)[1];
                    m.splice(j + 1, 0, index); // Push the index at m[j + 1]
                    var replacee = j === 0 ? item : params[i];
                    params[i] = replacee.split(m[j]).join(nested[m[j + 1]]); // Re-replace delimiters with original templates
                }
            }
        });
    }

    return params;

},

/**
 * Check if the current user belongs to a given user group
 * @param {string} group
 * @returns {boolean}
 */
inGroup: function(group) {
    if (paramsMissing(arguments, 1)) throwError('inGroup');
    return $.inArray(group, mw.config.get('wgUserGroups')) !== -1;
},

/**
 * Check if the current user belongs to a given global user group
 * @param {string} group
 * @returns {boolean}
 */
inGlobalGroup: function(group) {
    if (paramsMissing(arguments, 1)) throwError('inGlobalGroup');
    return mw.config.exists('wgGlobalGroups') && $.inArray(group, mw.config.get('wgGlobalGroups')) !== -1;
},

/**
 * Get the key of a value in an object
 * @param {Object} object 
 * @param {*} value 
 * @returns {string} key
 */
getKeyByValue: function(object, value) {
    if (paramsMissing(arguments, 2)) throwError('getKeyByValue');
    for (var key in object) {
        if (object[key] == value) return key;
    }
},

/**
 * Copy a string to the clipboard
 * @param {string} str
 */
copyToClipboard: function(str) {
    if (paramsMissing(arguments, 1)) throwError('copyToClipboard');
    var $temp = $('<textarea></textarea>');
    $('body').append($temp); // Create a temporarily hidden text field
    $temp.val(str).select(); // Copy the text string into the field and select the text
    document.execCommand('copy'); // Copy it to the clipboard
    $temp.remove(); // Remove the text field
},

/**
 * Add, remove, or move a loading spinner
 * @param {string} action 'add', 'remove', or 'move'
 */
toggleLoadingSpinner: function(action) {
    if (paramsMissing(arguments, 1)) throwError('toggleLoadingSpinner');
    if ($.inArray(action, ['add', 'remove', 'move']) === -1) throwInvalidParamError('toggleLoadingSpinner', action);
    var img = '<img class="dragolib-loading-spinner" src="https://upload.wikimedia.org/wikipedia/commons/4/42/Loading.gif" ' +
                'style="vertical-align: middle; height: 1em; border: 0;">';
    switch(action) {
        case 'add':
            return img;
        case 'remove':
            $('.dragolib-loading-spinner').remove();
            return '';
        case 'move':
            $('.dragolib-loading-spinner').remove();
            return img;
    }
},

/**
 * Get today's date in the form of MM月DD日
 * @returns {string}
 */
today: function() {
    var d = new Date();
    return (d.getMonth() + 1) + '月' + d.getDate() + '日';
},

/**
 * Get the last day of a given month
 * @param {number} year
 * @param {number} month
 * @returns {number}
 */
lastDay: function(year, month) {
    if (paramsMissing(arguments, 2)) throwError('lastDay');
    return new Date(year, month, 0).getDate();
},

/**
 * Get 'YYYY年MM月D1日 - D2日新規XX', corresponding to the current date
 * @param {string} suffix The XX part
 * @param {boolean} [last] If true, go back 5 days (get the name of the preceding section)
 * @returns {string} section name
 */
getSection5: function(suffix, last) {
    if (paramsMissing(arguments, 1)) throwError('getSection5');

    var d = new Date();
    var subtract;
    if (last) {
        if (d.getDate() === 1 || d.getDate() === 2) {
            subtract = 3;
        } else if (d.getDate() === 31) {
            subtract = 6;
        } else {
            subtract = 5;
        }
        d.setDate(d.getDate() - subtract);
    }

    var sectionName;
    switch (true) {
        case (1 <= d.getDate() && d.getDate() <= 5):
            sectionName = d.getFullYear() + '年' + (d.getMonth() + 1) + '月1日 - 5日新規' + suffix;
            break;
        case (6 <= d.getDate() && d.getDate() <= 10):
            sectionName = d.getFullYear() + '年' + (d.getMonth() + 1) + '月6日 - 10日新規' + suffix;
            break;
        case (11 <= d.getDate() && d.getDate() <= 15):
            sectionName = d.getFullYear() + '年' + (d.getMonth() + 1) + '月11日 - 15日新規' + suffix;
            break;
        case (16 <= d.getDate() && d.getDate() <= 20):
            sectionName = d.getFullYear() + '年' + (d.getMonth() + 1) + '月16日 - 20日新規' + suffix;
            break;
        case (21 <= d.getDate() && d.getDate() <= 25):
            sectionName = d.getFullYear() + '年' + (d.getMonth() + 1) + '月21日 - 25日新規' + suffix;
            break;
        case (26 <= d.getDate() && d.getDate() <= dragoLib.lastDay(d.getFullYear(), d.getMonth() + 1)):
            sectionName = d.getFullYear() + '年' + (d.getMonth() + 1) + '月26日 - ' + dragoLib.lastDay(d.getFullYear(), d.getMonth() + 1) + '日新規' + suffix;
            break;
        default:
    }
    return sectionName;

},

/**
 * Center a jQuery UI dialog (jQuery UI must be loaded)
 * @requires jquery.ui
 * @param {string} selectorName 
 */
centerDialog: function(selectorName) {
    if (paramsMissing(arguments, 1)) throwError('centerDialog');
    $(selectorName).dialog({position: {my: 'center', at: 'center', of: window}});
},

/**
 * Change the CSS of a jQuery UI dialog
 * @param {jQuery} $dialog $(dialogClass)
 * @param {string} headerColor 
 * @param {string} backgroundColor 
 * @param {string|number} [fontSize] 
 */
dialogCSS: function($dialog, headerColor, backgroundColor, fontSize) {
    if (paramsMissing(arguments, 3)) throwError('dialogCSS');
    $dialog.filter('.ui-dialog-content, .ui-corner-all, .ui-draggable, .ui-resizable').css('background', backgroundColor);
    $dialog.find('.ui-dialog-buttonpane').css('background', backgroundColor);
    $dialog.find('.ui-button').css({
        color: 'black',
        'background-color': 'white'
    });
    $dialog.find('.ui-dialog-titlebar, .ui-dialog-titlebar-close').attr('style', 'background: ' + headerColor + ' !important;');
    if (fontSize) $dialog.filter('.ui-dialog').css('font-size', fontSize);
},

/**
 * Get rid of U+200E spaces from a string and trim it
 * @param {string} str 
 * @returns {string}
 */
trim: function(str) {
    if (paramsMissing(arguments, 1)) throwError('trim');
    return str.replace(/\u200e/g, '').trim();
},

/**
 * Replace all occurences of a string with another
 * @param {string} str source string
 * @returns {string}
 */
replaceAll: function(str) { // Takes a replacee and a replacer (iterable)
    if (paramsMissing(arguments, 3)) throwError('replaceAll');
    if (arguments.length % 2 !== 1) {
        throw 'ReferenceError: dragoLib.replaceAll takes an odd number of arguments.';
    } else {
        for (var i = 1; i < arguments.length; i += 2) {
            str = str.split(arguments[i]).join(arguments[i + 1]);
        }
        return str;
    }
},

/**
 * Escape regular expressions
 * @param {string} str 
 * @param {boolean} [escapePipes]
 * @returns {string}
 */
escapeRegExp: function(str, escapePipes) {
    if (paramsMissing(arguments, 1)) throwError('escapeRegExp');
    if (typeof str !== 'string') throwTypeError('escapeRegExp', 'String');
    var rep = ['\\', '(', ')', '{', '}', '.', '?', '!', '*', '+', '-', '^', '$', '[', ']'];
    if (escapePipes) rep.push('|');
    for (var i = 0; i < rep.length; i++) {
        str = str.split(rep[i]).join('\\' + rep[i]);
    }
    return str;
},

/**
 * Parse the content of a page and get information of each section
 * @param {string} pageContent
 * @returns {Array<{header: string, title: string, level: number, index: number, content: string, deepest: boolean}>}
 */
parseContentBySection: function(pageContent) {
    if (paramsMissing(arguments, 1)) throwError('parseContentBySection');

    var regex = {
        header: /={2,5}[^\S\n\r]*.+[^\S\n\r]*={2,5}?/,
        headerG: /={2,5}[^\S\n\r]*.+[^\S\n\r]*={2,5}?/g,
        headerEquals: /(?:^={2,5}[^\S\n\r]*|[^\S\n\r]*={2,5}$)/g
    };

    var content = JSON.parse(JSON.stringify(pageContent));
    var headers = content.match(regex.headerG);

    var co = dragoLib.extractCommentOuts(content);
    if (co) {
        co.forEach(function(item) {
            headers = headers.filter(function(header) {
                return item.indexOf(header) === -1;
            });
        });
    }

    var sections = [];
    sections.push({ // The top section
        header: null,
        title: null,
        level: 1,
        index: 0,
        content: headers ? content.split(headers[0])[0] : content,
        deepest: null
    });
    if (headers) {
        headers.forEach(function(header) {
            sections.push({
                header: header,
                title: header.replace(regex.headerEquals, ''),
                level: header.match(/=/g).length / 2,
                index: undefined,
                content: undefined,
                deepest: undefined
            });
        });
        sections.forEach(function(obj, i, arr) {
            if (i === 0) return;
            var sectionContent;
            arr.slice(i + 1).some(function(obj2) { // Find a higher-level section boundary and get the content between the header and the boundary
                if (obj2.level <= obj.level) sectionContent = content.substring(content.indexOf(obj.header), content.indexOf(obj2.header));
                return typeof sectionContent !== 'undefined';
            });
            if (!sectionContent) sectionContent = content.substring(content.indexOf(obj.header)); // For last section
            obj.index = i;
            obj.content = sectionContent;
            obj.deepest = obj.content.match(regex.header).length === 1;
        });
    }

    return sections;

},

/**
 * Check whether the current user has an 'apihighlimit' user right
 * @returns {boolean}
 */
apiHighLimit: function() {
    var groupsAHL = ['bot', 'sysop', 'apihighlimits-requestor', 'founder', 'global-bot', 'global-sysop', 'staff', 'steward', 'sysadmin', 'wmf-researcher'];
    return [].concat(mw.config.get('wgUserGroups'), mw.config.get('wgGlobalGroups')).some(function(group) {
        return groupsAHL.indexOf(group) !== -1;
    });
},

/**
 * Compare two page titles
 * @param {string} title1 
 * @param {string} title2 
 * @returns {boolean}
 */
isSameTitle: function(title1, title2) {
    if (paramsMissing(arguments, 2)) throwError('isSameTitle');

    title1 = dragoLib.trim(title1);
    title2 = dragoLib.trim(title2);

    var nsIds = mw.config.get('wgNamespaceIds'); // e.g. {category: 14, category_talk: 15, ...}
    var regexNsPrefixes = Object.keys(nsIds).map(function(el) { return mw.util.escapeRegExp(el).replace(/_/g, '[ _]') + '\\s*:'; });
    regexNsPrefixes = new RegExp('^(' + regexNsPrefixes.join('|') + ')','i');

    var getNamespaceNumberFromPrefixedTitle = function(pagetitle) {
        var m;
        if (!(m = pagetitle.match(regexNsPrefixes))) return 0;
        var prefix = m[0].replace(/\s*:$/, ':').replace(/ /g, '_').toLowerCase();
        return nsIds[prefix];
    };

    var ns;
    var titles = {
        1: {
            input: title1,
            namespace: (ns = getNamespaceNumberFromPrefixedTitle(title1)),
            page: ns === 0 ? title1 : title1.replace(/^.+:/, '').trim()
        },
        2: {
            input: title2,
            namespace: (ns = getNamespaceNumberFromPrefixedTitle(title2)),
            page: ns === 0 ? title2 : title2.replace(/^.+:/, '').trim()
        }
    };
    if (titles[1].namespace !== titles[2].namespace) return false;

    var firstLet = titles[1].page.substring(0, 1);
    var rest = '';
    if (titles[1].page.length !== 1) {
        rest = mw.util.escapeRegExp(titles[1].page.substring(1));
    }
    var regexPagename = new RegExp('^[' + firstLet.toLowerCase() + firstLet.toUpperCase() + ']' + rest.replace(/[ _]/g, '[ _]') + '$');
    return titles[2].page.match(regexPagename) ? true : false;

},

// ******************************** ASYNCHRONOUS FUNCTIONS ********************************

/**
 * Get the latest revision(s) of page(s)
 * @requires mediawiki.api
 * @param {string|Array} pagenames
 * @returns {jQuery.Promise<Array<{title: string, missing: boolean, revid: string, basetimestamp: string, curtimestamp: string, content: string}>>}
 */
getLatestRevision: function(pagenames) {

    var def = new $.Deferred();
    if (!pagenames) return def.reject(mw.log.error('getLatestRevision: The pagenames parameter is undefined.'));
    var pagetitles = !Array.isArray(pagenames) ? [pagenames] : pagenames;

    new mw.Api().get({
        action: 'query',
        titles: pagetitles.join('|'),
        prop: 'revisions',
        rvprop: 'timestamp|content|ids',
        rvslots: 'main',
        curtimestamp: 1,
        formatversion: '2'
    }).then(function(res) {

        var errHandler = function() {
            mw.log.warn('Failed to get the latest revision.');
        };
        var resPages;
        if (!res || !res.query || !(resPages = res.query.pages)) return def.resolve(errHandler());
        if (resPages.length === 0) return def.resolve(errHandler());

        var resArray = resPages.reduce(function(acc, obj) {
            acc.push({
                title: obj.title,
                missing: obj.missing,
                revid: obj.missing ? undefined : obj.revisions[0].revid.toString(),
                basetimestamp: obj.missing ? undefined : obj.revisions[0].timestamp,
                curtimestamp: res.curtimestamp,
                content: obj.missing ? undefined : obj.revisions[0].slots.main.content
            });
            return acc;
        }, []);

        def.resolve(resArray);

    }).catch(function(code, err) {
        def.resolve(mw.log.warn('getLatestRevision: ' + err.error.info));
    });

    return def.promise();

},

/**
 * Edit a page
 * @requires mediawiki.api
 * @param {{}} params Don't specify the format parameter.
 * @param {boolean} [causeError] 
 * @returns {jQuery.Promise<boolean|string>} true if edit succeeds, false if it fails due to an unknown error, string if it fails due to a known error
 */
editPage: function(params, causeError) {
    var def = new $.Deferred();

    if (causeError) return def.resolve('Intentional error caused for debugging.');
    new mw.Api().postWithEditToken(params)
    .then(function(res) {
        def.resolve(res && res.edit && res.edit.result === 'Success');
    }).catch(function(code, err) {
        var e = err && err.error && err.error.info ? err.error.info : null;
        mw.log.warn(e ? e : err);
        def.resolve(e ? e : 'Unknown error.');
    });

    return def.promise();
},

/**
 * Check if a user exists locally
 * @requires mediawiki.api
 * @param {string} username 
 * @returns {jQuery.Promise<boolean|undefined>}
 */
userExists: function(username) {
    var def = new $.Deferred();
    if (paramsMissing(arguments, 1)) throwError('userExists');

    new mw.Api().get({
        action: 'query',
        list: 'users',
        ususers: username,
        formatversion: 2
    }).then(function(res){
        var resUs;
        def.resolve(res && res.query && (resUs = res.query.users) && resUs[0] && typeof resUs[0].userid !== 'undefined');
    }).catch(function(code, err) {
        def.resolve(mw.log.warn(err.error.info));
    });

    return def.promise();
},

/**
 * Convert wikitext to html
 * @requires mediawiki.api
 * @param {string} wikitext
 * @param {string} [wikisummary]
 * @returns {jQuery.Promise<{htmltext: string, htmlsummary: string}>}
 */
getParsedHtml: function(wikitext, wikisummary) {
    var def = new $.Deferred();
    if (paramsMissing(arguments, 1)) throwError('getParsedHtml');

    new mw.Api().post({
        action: 'parse',
        text: wikitext,
        summary: typeof wikisummary === 'undefined' ? '' : wikisummary,
        contentmodel: 'wikitext',
        prop: 'text',
        disableeditsection: true,
        formatversion: 2
    }).then(function(res) {
        if (res && res.parse) {
            return def.resolve({
                'htmltext': res.parse.text,
                'htmlsummary': res.parse.parsedsummary
            });
        }
        def.resolve();
    }).catch(function(code, err) {
        def.resolve(mw.log.warn(err.error.info));
    });

    return def.promise();
},

/**
 * Watch pages
 * @requires mediawiki.api
 * @param {Array} pagesArr 
 * @param {boolean} [unwatch]
 * @param {string} [expiry]
 * @returns {jQuery.Promise<Array>} Array of (un)watched pages
 */
watchPages: function(pagesArr, unwatch, expiry) {
    var def = new $.Deferred();

    var pgArr = JSON.parse(JSON.stringify(pagesArr)); // Prevent pass-by-reference (the variable will be spliced)
    if (paramsMissing(arguments, 1)) throwError('watchPages');
    if (!Array.isArray(pgArr)) throwTypeError('watchPages', 'Array');
    if (pgArr.length === 0) {
        mw.log.warn('dragoLib.watchPages: The passed array is empty.');
        return def.resolve([]);
    }

    var watched = [];
    var query = function(pages) {
        var deferred = new $.Deferred();

        var params = {
            action: 'watch',
            titles: pages.join('|'),
            formatversion: 2
        };
        if (unwatch) params.unwatch = true;
        if (expiry) params.expiry = expiry;

        new mw.Api().postWithToken('watch', params)
        .then(function(res) {
            var resWtch;
            if (!res || !(resWtch = res.watch)) return deferred.resolve(mw.log.warn('dragoLib.watchPages: Unexpected error occurred on a watch-pages attempt.'));
            var w = resWtch.filter(function(obj) { return obj.watched; }).map(function(obj) { return obj.title; });
            watched = watched.concat(w);
            deferred.resolve();
        }).catch(function(code, err) {
            mw.log.warn('dragoLib.watchPages: Error occurred on a watch-pages attempt: ' + err.error.info);
            deferred.resolve();
        });

        return deferred.promise();
    };

    var apihighlimit = mw.config.get('wgUserGroups').some(function(group) {
        return ['bot', 'sysop'].indexOf(group) !== -1;
    });
    var apilimit = apihighlimit ? 500 : 50;
    var deferreds = [];
    while (pgArr.length) {
        deferreds.push(query(pgArr.splice(0, apilimit)));
    }
    $.when.apply($, deferreds).then(function() {
        watched = watched.filter(function(el, i, arr) { return arr.indexOf(el) === i; });
        def.resolve(watched);
    });

    return def.promise();
},

/**
 * Get the local block, global block, and lock statues of users by scraping contribs (slow)
 * @param {string|Array} usernames 
 * @returns {jQuery.Promise<Array<{user: string, blocked: boolean, gblocked: boolean, locked: boolean}>>}
 */
queryBlockStatus: function(usernames) {
    var def = new $.Deferred();

    if (typeof usernames === 'string') usernames = [usernames];

    var query = function(user) {
        var deferred = new $.Deferred();

        var url = '/wiki/特別:投稿記録/' + user;
        $.get(url, function(scraped) {

            if (scraped) {
                var blocked = false,
                    gblocked = false,
                    locked = false,
                    $dom = $(scraped),
                    $warning = $dom.find('.mw-message-box-warning');
                if ($dom.find('.mw-contributions-blocked-notice').length !== 0) blocked = true;
                if ($warning.filter(function() { return $(this).text().indexOf('このIPアドレスは現在グローバルブロックされています') !== -1; }).length !== 0) gblocked = true;
                if ($warning.filter(function() { return $(this).text().indexOf('このアカウントはグローバルロックされています') !== -1; }).length !== 0) locked = true;
            }

            deferred.resolve({
                user: user,
                blocked: blocked,
                gblocked: gblocked,
                locked: locked
            });

        });
        return deferred.promise();
    };

    var q = [];
    usernames.forEach(function(u) {
        q.push(query(u));
    });
    $.when.apply($, q).then(function() {
        var args = arguments;
        var arr = Object.keys(args).map(function(key) { return args[key]; });
        def.resolve(arr);
    });

    return def.promise();
},

/**
 * Get an array of users and IPs that are banned from editing (in any way) from an array of random users and IPs
 * @requires mediawiki.util
 * @requires mediawiki.api
 * @param {Array} namesArr 
 * @returns {jQuery.Promise<Array<string>>}
 */
getRestricted: function(namesArr) {
    var def = new $.Deferred();

    if (paramsMissing(arguments, 1)) throwError('getRestricted');
    if (!Array.isArray(namesArr)) throwTypeError('getRestricted', 'Array');
    if (namesArr.length === 0) {
        mw.log.warn('dragoLib.getRestricted: The passed array is empty.');
        return def.resolve([]);
    }

    var users = [], ips = [];
    namesArr.forEach(function(name) {
        if (mw.util.isIPAddress(name, true)) {
            ips.push(name);
        } else {
            users.push(name);
        }
    });

    var deferreds = [];
    if (users.length > 0) deferreds.push(dragoLib.getBlockedUsers(users), dragoLib.getLockedUsers(users));
    if (ips.length > 0) deferreds.push(dragoLib.getBlockedIps(ips), dragoLib.getGloballyBlockedIps(ips));
    $.when.apply($, deferreds).then(function() {
        var args = arguments;
        var restricted = Object.keys(args).map(function(key) { return args[key]; });
        restricted = [].concat.apply([], restricted).filter(function(u, i, arr) { return arr.indexOf(u) === i; });
        def.resolve(restricted);
    });

    return def.promise();
},

/**
 * Get an array of blocked users from an array of users and IPs (Note: This function does not detect single IPs in blocked ranges)
 * @requires mediawiki.api
 * @param {Array} usersArr
 * @returns {jQuery.Promise<Array<string>>} Locally blocked users and IPs
 */
getBlockedUsers: function(usersArr) {
    var def = new $.Deferred();

    var users = JSON.parse(JSON.stringify(usersArr)); // Prevent pass-by-reference (the variable will be spliced)
    if (paramsMissing(arguments, 1)) throwError('getBlockedUsers');
    if (!Array.isArray(users)) throwTypeError('getBlockedUsers', 'Array');
    if (users.length === 0) {
        mw.log.warn('dragoLib.getBlockedUsers: The passed array is empty.');
        return def.resolve([]);
    }

    var blocked = [];
    var query = function(arr) {
        var deferred = new $.Deferred();

        new mw.Api().post({
            action: 'query',
            list: 'blocks',
            bklimit: 'max',
            bkusers: arr.join('|'),
            bkprop: 'user',
            formatversion: 2
        }).then(function(res) {
            var resBlck;
            if (res && res.query && (resBlck = res.query.blocks)) {
                var u = resBlck.map(function(obj) { return obj.user; });
                blocked = blocked.concat(u);
            } else {
                mw.log.warn('dragoLib.getBlockedUsers: Unexpected error occurred on a check-blocks attempt.');
            }
            deferred.resolve();
        }).catch(function(code, err) {
            mw.log.warn('dragoLib.getBlockedUsers: Error occurred on a check-blocks attempt: ' + err.error.info);
            deferred.resolve();
        });

        return deferred.promise();
    };

    var apihighlimit = mw.config.get('wgUserGroups').some(function(group) {
        return ['bot', 'sysop'].indexOf(group) !== -1;
    });
    var apilimit = apihighlimit ? 500 : 50;
    var deferreds = [];
    while (users.length) {
        deferreds.push(query(users.splice(0, apilimit)));
    }
    $.when.apply($, deferreds).then(function() {
        def.resolve(blocked);
    });

    return def.promise();
},

/**
 * Get an array of locally blocked IPs from an array of random IPs (can detect range blocks)
 * @requires mediawiki.api
 * @requires mediawiki.util
 * @param {Array} ipsArr 
 * @returns {jQuery.Promise<Array<string>>} Locally blocked IPs
 */
getBlockedIps: function(ipsArr) {
    var def = new $.Deferred();

    if (paramsMissing(arguments, 1)) throwError('getBlockedIps');
    if (!Array.isArray(ipsArr)) throwTypeError('getBlockedIps', 'Array');
    if (ipsArr.length === 0) {
        mw.log.warn('dragoLib.getBlockedIps: The passed array is empty.');
        return def.resolve([]);
    }

    var deferreds = [];
    ipsArr = ipsArr.filter(function(ip, i, arr) { return arr.indexOf(ip) === i; });
    ipsArr.forEach(function(ip) { deferreds.push(dragoLib.isBlocked(ip)); });
    $.when.apply($, deferreds).then(function() {
        var args = arguments;
        var blocked = Object.keys(args).map(function(key) { return args[key]; });
        blocked = ipsArr.filter(function(ip, i) { return blocked[i]; });
        def.resolve(blocked);
    });

    return def.promise();
},

/**
 * Check if a user is locally blocked
 * @requires mediawiki.api
 * @requires mediawiki.util
 * @param {string} user Can be any of a registered user, an IP, or an IP range
 * @returns {jQuery.Promise<boolean|undefined>}
 */
isBlocked: function(user) {
    var def = new $.Deferred();

    if (paramsMissing(arguments, 1)) throwError('isBlocked');
    if (typeof user !== 'string') throwTypeError('isBlocked', 'String');

    var params = {
        action: 'query',
        list: 'blocks',
        bklimit: 1,
        bkprop: 'user',
        formatversion: 2
    };
    var usertype = mw.util.isIPAddress(user, true) ? 'bkip' : 'bkusers';
    params[usertype] = user;
    new mw.Api().get(params)
    .then(function(res) {
        var resBlck;
        if (res && res.query && (resBlck = res.query.blocks)) {
            def.resolve(resBlck.length !== 0);
        } else {
            mw.log.error('dragoLib.isBlocked: Unexpected error occurred on a check-block attempt.');
            def.resolve();
        }
    }).catch(function(code, err) {
        mw.log.error('dragoLib.isBlocked: Error occurred on a check-block attempt: ' + err.error.info);
        def.resolve();
    });

    return def.promise();
},

/**
 * Get an array of locked users from an array of registered users
 * @requires mediawiki.api
 * @param {Array} regUsersArr 
 * @returns {jQuery.Promise<Array<string>>} Globally locked users
 */
getLockedUsers: function(regUsersArr) {
    var def = new $.Deferred();

    if (paramsMissing(arguments, 1)) throwError('getLockedUsers');
    if (!Array.isArray(regUsersArr)) throwTypeError('getLockedUsers', 'Array');
    if (regUsersArr.length === 0) {
        mw.log.warn('dragoLib.getLockedUsers: The passed array is empty.');
        return def.resolve([]);
    }

    var deferreds = [];
    regUsersArr = regUsersArr.filter(function(user, i, arr) { return arr.indexOf(user) === i; });
    regUsersArr.forEach(function(user) { deferreds.push(dragoLib.isLocked(user)); });
    $.when.apply($, deferreds).then(function() {
        var args = arguments;
        var locked = Object.keys(args).map(function(key) { return args[key]; });
        locked = regUsersArr.filter(function(user, i) { return locked[i]; });
        def.resolve(locked);
    });

    return def.promise();
},

/**
 * Check if a user is globally locked
 * @requires mediawiki.api
 * @param {string} user 
 * @returns {jQuery.Promise<boolean|undefined>}
 */
isLocked: function(user) {
    var def = new $.Deferred();

    if (paramsMissing(arguments, 1)) throwError('isLocked');
    if (typeof user !== 'string') throwTypeError('isLocked', 'String');

    new mw.Api().get({
        action: 'query',
        list: 'globalallusers',
        agulimit: 1,
        agufrom: user,
        aguto: user,
        aguprop: 'lockinfo'
    }).then(function(res) {
        var resLck;
        if (res && res.query && (resLck = res.query.globalallusers)) {
            def.resolve(resLck.length === 0 ? false : resLck[0].locked !== undefined); // resLck[0].locked === '' if locked, otherwise undefined
        } else {
            mw.log.error('dragoLib.isLocked: Unexpected error occurred on a check-lock attempt.');
            def.resolve();
        }
    }).catch(function(code, err) {
        mw.log.error('dragoLib.isLocked: Error occurred on a check-lock attempt: ' + err.error.info);
        def.resolve();
    });

    return def.promise();
},

/**
 * Function to get an array of globally blocked IPs from an array of random IPs
 * @requires mediawiki.api
 * @param {Array} ipsArr
 * @returns {jQuery.Promise<Array<string>>} Globally blocked IPs
 */
getGloballyBlockedIps: function(ipsArr) {
    var def = new $.Deferred();

    if (paramsMissing(arguments, 1)) throwError('getGloballyBlockedIps');
    if (!Array.isArray(ipsArr)) throwTypeError('getGloballyBlockedIps', 'Array');
    if (ipsArr.length === 0) {
        mw.log.warn('dragoLib.getGloballyBlockedIps: The passed array is empty.');
        return def.resolve([]);
    }

    var deferreds = [];
    ipsArr = ipsArr.filter(function(ip, i, arr) { return arr.indexOf(ip) === i; });
    ipsArr.forEach(function(ip) { deferreds.push(dragoLib.isIpGloballyBlocked(ip)); });
    $.when.apply($, deferreds).then(function() {
        var args = arguments;
        var gblocked = Object.keys(args).map(function(key) { return args[key]; });
        gblocked = ipsArr.filter(function(ip, i) { return gblocked[i]; });
        def.resolve(gblocked);
    });

    return def.promise();
},

/**
 * Check if a given IP is globally blocked (can detect range blocks)
 * @requires mediawiki.api
 * @param {string} ip 
 * @returns {jQuery.Promise<boolean|undefined>}
 */
isIpGloballyBlocked: function(ip) {
    var def = new $.Deferred();

    if (paramsMissing(arguments, 1)) throwError('isIpGloballyBlocked');
    if (typeof ip !== 'string') throwTypeError('isIpGloballyBlocked', 'String');

    new mw.Api().get({
        action: 'query',
        list: 'globalblocks',
        bgip: ip,
        bglimit: 1,
        bgprop: 'address',
        formatversion: 2
    }).then(function(res) {
        var resGb;
        if (res && res.query && (resGb = res.query.globalblocks)) {
            def.resolve(resGb.length !== 0);
        } else {
            mw.log.error('dragoLib.isIpGloballyBlocked: Unexpected error occurred on a check-block attempt.');
            def.resolve();
        }
    }).catch(function(code, err) {
        mw.log.error('dragoLib.isIpGloballyBlocked: Error occurred on a check-block attempt: ' + err.error.info);
        def.resolve();
    });

    return def.promise();
}

// ****************************************************************
};
if (typeof module !== 'undefined' && module.exports) {
    module.exports.dragoLib = dragoLib;
}

// ******************************** LIBRARY FUNCTIONS ********************************
// The following functions are for the library itself and not included in dragoLib

/**
 * @param {*} args Always pass 'arguments'
 * @param {number} stopAt Number of necessary parameters (all optional parameters should follow necessary parameters)
 * @returns {boolean}
 */
function paramsMissing(args, stopAt) {
    for (var i = 0; i < stopAt; i++) {
        if (typeof args[i] === 'undefined') return true;
    }
    return false;
}

/**
 * @param {string} functionName
 */
function throwError(functionName) {
    throw mw.log.error('Uncaught ReferenceError: Necessary parameter(s) not set. (dragoLib.' + functionName + ')');
}

/**
 * @param {string} functionName
 * @param {*} param
 */
function throwInvalidParamError(functionName, param) {
    throw mw.log.error('Uncaught ReferenceError: ' + param + ' is an invalid parameter. (dragoLib.' + functionName + ')');
}

/**
 * @param {string} functionName
 * @param {string} type
 */
function throwTypeError(functionName, type) {
    throw mw.log.error('Uncaught TypeError: ' + type + ' must be passed to dragoLib.' + functionName + '.');
}

}
//</nowiki>