MediaWiki:Wikimarks.js

From Coral Island Wiki
Revision as of 05:48, 12 July 2024 by Mikevoir (talk | contribs) (Created page with "/** * Wikimarks v2 * Replaces the original version following it being broken by verbatim being disabled * * @author Pecoes <https://c.fandom.com/wiki/User:Pecoes> * @author Cqm <https://c.fandom.com/wiki/User:Cqm> * * Changes from the original: * - No longer uses verbatim, thus should be more stable. * - No longer allows javascript: URLs (and thus custom functions) * - Supports wikitext as well as (most of) the old syntax * * New Wiki Nav Todos: * - Test...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/**
 * Wikimarks v2
 * Replaces the original version following it being broken by verbatim being disabled
 *
 * @author Pecoes <https://c.fandom.com/wiki/User:Pecoes>
 * @author Cqm    <https://c.fandom.com/wiki/User:Cqm>
 *
 * Changes from the original:
 * - No longer uses verbatim, thus should be more stable.
 * - No longer allows javascript: URLs (and thus custom functions)
 * - Supports wikitext as well as (most of) the old syntax
 *
 * New Wiki Nav Todos:
 * - Test for touchscreens?
 *
 * Used files:
 * - [[File:Facebook throbber.gif]]
 */

/*global importArticle */

/*jshint
    bitwise:true, camelcase:true, curly:true, eqeqeq:true, es3:false,
    forin:true, immed:true, indent:4, latedef:true, newcap:true,
    noarg:true, noempty:true, nonew:true, plusplus:true, quotmark:single,
    undef:true, unused:true, strict:true, trailing:true,
    browser:true, devel:false, jquery:true,
    onevar:true
*/

;(function ($, mw, dev) {
    'use strict';

    var conf = mw.config.get([
    		'skin',
            'stylepath',
            'wgPageName',
            'wgScriptPath',
            'wgServer',
            'wgUserName'
        ]),

        testUser = false;

    /**
     * Insert Wikimarks into the DOM and attach the relevant events
     */
    function addHtml($menu) {
        var $wikimarks = $('#mw-navigation > .collapsible-nav');

        $wikimarks.append($menu);

        // everything is now done
        // so fire an event so people can interact/extend it further
        mw.hook('wikimarks.loaded').fire($wikimarks);
    }

    /**
     * Prepare the parsed HTML and attach to the DOM
     */
    function prepareHtml(html) {
        var $parsed = $(html);

        // remove the parser output wrapping element
        if ($parsed.hasClass('mw-parser-output')) {
            $parsed = $parsed.children();
        }

        var $menu = $('<nav id="p-Wikimarks" role="navigation" aria-labelledby="p-Wikimarks-label"></nav>')
                .addClass('vector-menu mw-portlet mw-portlet-Wikimarks vector-menu-portal portal expanded')
                .append(
                	$('<h3></h3>', {id:'p-Wikimarks-label', 'class':'vector-menu-heading', tabindex: '0'})
                		.append('<a href="#" aria-haspopup="true" aria-controls="p-Wikimarks-list" role="button" aria-pressed="true" aria-expanded="true"><span class="vector-menu-heading-label">Wikimarks</span></a>'),
	                $('<div></div>').addClass('vector-menu-content').append($parsed)
	             );

        // add classes to elements
        $menu
            .children('div.vector-menu-content')
            .children('ul')
                .addClass('vector-menu-content-list')
                .attr('id', 'p-Wikimarks-list')
                .children('li')
                    .children('a')
                        .siblings('ul')
                        .remove(); // disallow nesting

        // remove href from text converted to links
        $menu.find('a[href="' + conf.wgScriptPath + '/wiki/"]')
            .removeAttr('href')
            .css('cursor', 'pointer');


        $menu.find('a')
            // titles don't add anything to the links
            .removeAttr('title')
            // remove external link class for ease of reading the source html
            .removeClass('extiw');

        if (testUser) {
            return;
        }

        addHtml($menu);
    }

    /**
     * Pass the preprocess wikimarks to action=parse to be converted into wikitext
     */
    function parseWikimarks(data) {
        var params = {
            action: 'parse',
            contentmodel: 'wikitext',
            prop: 'text',
            text: data
        };

        (new mw.Api())
            .post(params)
            .done(function (data) {
                var text = data.parse.text['*'];

                // remove preprocessor comment
                // should be able to hide it in api config
                // but that's broken in mw1.19
                text = text.replace(/<!--[\s\S]*?-->/g, '').trim();

                if (testUser) {
                    mw.log(text);
                }

                prepareHtml(text);
            });
    }

    /**
     * Preprocesses a wikimarks page to make it compatible with the wikitext parser
     */
    function preprocessData(data) {
        data = data.trim().split(/\n+/);

        var invalidLink = false,
            parsed = [],
            // handles:
            // - /wiki/ (wiki pages)
            // - index.php, api.php, and wikia.php (API)
            // - /f and /d (discussions)
            relativeUrlRe = /\/(wiki\/|(?:index|api|wikia)\.php|f|d)/;

        data.forEach(function (elem) {
            // ignore comments
            if (elem.indexOf('//') === 0 || elem.indexOf('#') === 0) {
                return;
            }

            // handle external links
            elem = elem.replace(/^(\*+)\s*\[([^\s]+)\s+(.+?)\]\s*$/, function (_, p1, p2, p3) {
                // handle query strings
                if (p2.indexOf('?') === 0) {
                    return p1 + '[{{fullurl:' + conf.wgPageName + '|' + p2.slice(1) + '}} ' + p3 + ']';
                }

                // allow appending to existing query strings as well
                if (p2.indexOf('&') === 0) {
                    return p1 + '[' + location.href + p2 + ' ' + p3 + ']';
                }

                // handle relative URLs
                if (p2.search(relativeUrlRe) === 0) {
                    p2 = conf.wgServer + conf.wgScriptPath + p2;
                }

                // else just return it unchanged
                return p1 + ' [' + p2 + ' ' + p3 + ']';
            });

            // don't touch raw html
            // assumes that all html will begin with a tag, e.g. <span...
            if (!/^\*+\s*</.test(elem)) {
                // parse old style links to wikitext for backwards compatibility
                elem = elem.replace(/^(\*+)\s*([^\[]+?)\s*=\s*(.+?)\s*$/, function (_, p1, p2, p3) {
                    // handle absolute URLs
                    // 'http://' or 'https://' or '//'
                    if (p3.search(/(?:https?:)?\/\//) === 0) {
                        return p1 + ' [' + p3 + ' ' + p2 + ']';
                    }

                    // handle query strings
                    if (p3.indexOf('?') === 0) {
                        return p1 + '[{{fullurl:' + conf.wgPageName + '|' + p3.slice(1) + '}} ' + p2 + ']';
                    }

                    // allow appending to existing query strings as well
                    if (p2.indexOf('&') === 0) {
                        return p1 + '[' + location.href + p2 + ' ' + p3 + ']';
                    }

                    // attempt to fix instances of Foo?bar=baz
                    // domain added below
                    if (p3.indexOf('?') > -1) {
                        p3 = '/wiki/' + p3;
                    }

                    // handle relative URLs
                    if (p3.search(relativeUrlRe) === 0) {
                        p3 = conf.wgServer + conf.wgScriptPath + p3;
                        return p1 + ' [' + p3 + ' ' + p2 + ']';
                    }

                    // ## BREAKING CHANGE ##
                    // don't allow 'javascript:' urls
                    // ridiculously difficult to parse these in js without using `eval`
                    if (p3.search(/(?:javascript:)?(?:url|win)\(/) === 0) {
                        p3 = '#invalidLink';
                        invalidLink = true;
                    }

                    // else we expect a normal wikilink
                    return p1 + ' [[' + p3 + '|' + p2 + ']]';
                });
            }

            // remove css comment
            // caused by loading wikimarks config through RL and pretending it's CSS
            if (elem.search(/^\/\*.+?\*\/$/) === 0) {
                elem = '';
            }

            // substitute in global variables
            // syntax: {$VAR} where VAR is a global variable
            // @todo limit to stuff available in mw.config?
            elem = elem.replace(/\{\$(.+?)\}/g, function (_, p1) {
                // fix for properties of globals
                var parts = p1.split('.'),
                    test = window,
                    prop,
                    i;

                for (i = 0; i < parts.length; i += 1) {
                    prop = parts[i];

                    // @todo how secure is this?
                    if (test.hasOwnProperty(prop)) {
                        test = test[prop];
                    } else {
                        break;
                    }
                }

                if (['string', 'number'].indexOf(typeof test) > -1) {
                    return test;
                } else {
                	return mw.config.get(p1);
                }
            });

            // make simple text strings into a null link so it doesn't break the styling
            elem = elem.replace(/^(\*+)\s*([A-Za-z0-9\s]+)\s*$/, '$1 [[#|$2]]');

            parsed.push(elem);
        });

        data = parsed.join('\n').trim();
        mw.log(data);

        if (invalidLink) {
            // @todo do something
        }

        return data;
    }

    /**
     * Load the users wikimarks
     */
    function loadWikimarks(username) {
        var load = 'https://coralisland.wiki/api.php',
            params = {
                action: 'query',
                format: 'json',
                prop: 'revisions',
                rvprop: 'content',
                // don't encode anything in the username here, $.ajax does it anyway
                // otherwise stuff gets encoded twice and no results are returned
                titles: 'User:' + (username || conf.wgUserName).replace(/ /g, '_') + '/Wikimarks',
                indexpageids: 1,
                origin: '*',
                // Cache results for 5 minutes in CDN and browser
                maxage: 300,
                smaxage: 300
            };

        if (username) {
            testUser = true;
        }

        mw.log('params', params);

        $.ajax(load, {
            data: params
        }).always(function (data) {
            var res = '',
                revisionData = data.query && data.query.pages[data.query.pageids[0]].revisions;

            if (revisionData && revisionData.length>0) {
                res = revisionData[0]['*'];
            } else {
            	return; // No wikimarks, end
            }

            res = preprocessData(res);
            parseWikimarks(res);
        });
    }

    /**
     * Shows loading status until the wikimarks have loaded
     */
    function showLoading() {
        var $nav = $('.wds-community-header__local-navigation .wds-tabs, .fandom-community-header__local-navigation .wds-tabs'),
            $li = $('<li>');

        $li.addClass('wds-tabs__tab wikimarks')
            .css({
                backgroundImage: 'url("https://vignette.wikia.nocookie.net/dev/images/8/82/Facebook_throbber.gif")',
                backgroundPosition: 'center center',
                backgroundRepeat: 'no-repeat',
            })
            .append(
                $('<div>')
                    .addClass('wds-dropdown')
                    .append(
                        $('<div>')
                            .addClass('wds-tabs__tab-label wds-dropdown__toggle first-level-item')
                            .append(
                                $('<a>')
                                    .attr(
                                        'href',
                                        'https://dev.fandom.com/wiki/User:' + conf.wgUserName + '/Wikimarks'
                                    )
                                    .css('visibility', 'hidden')
                                    .append(
                                        $('<span>')
                                            .text('WIKIMARKS')
                                    )
                            )
                    )
            );

        // hide the explore tab (the new "on the wiki" tab)
        // TODO: send in a ticket to get a class for this
        //       as it feels super fragile
        
        // find the list with "random page" link and hide the whole list (explore tab)
        $('.wds-list [data-tracking="explore-random"]').closest('.wds-dropdown').hide();
        // add our new tab to the start of the nav
        $nav.prepend($li);
    }

    /**
     * Load stylesheets
     */
    function loadStyles() {
        mw.util.addCSS(
        	'.wikimarks a[data-uncrawlable-url], .wikimarks span[data-uncrawlable-url] {'+
				'align-items: center;'+
				'border-radius: 3px;'+
				'display: flex;'+
				'line-height: 1.75em;'+
				'padding: 9px 6px;'+
			'}'
		);
    }

    /**
     * Checks for the correct environment before allowing the script to continue
     */
    function init() {
        // prevent anyone trying to load this for anons
        if (!conf.wgUserName) {
            return;
        }

        if (!$('.mw-navigation').length) {
            mw.log('Wikimarks: sidebar not found, aborting...');
            return;
        }
        
        loadStyles();
        loadWikimarks();
    }

    mw.loader.using(['mediawiki.api', 'mediawiki.util'], function () {
        $(init);
    });

    dev.loadWikimarks = loadWikimarks;

}(this.jQuery, this.mediaWiki, this.dev = this.dev || {}));