MediaWiki:Gadget-updates-core.js
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)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/** <pre>
* Implements a script that allows users to create new update pages easily
*
* @author Joeytje50
* @author MrDew
* @author JaydenKieran
* @author Elessar2
*
* TODO: show list of IMGs to be uploaded for confirmation? maybe even small clickable thumbnails
* TODO: btw, mw.notify( $.parseHTML( "lol<br>ok" ), { ... } ); works... :x
*/
/*jshint bitwise: true, eqeqeq: true, esversion: 6, forin: true, freeze: true,
futurehostile: true, immed: true, latedef: true, noarg: true, nonew: true,
strict: true, undef: true, unused: true, browser: true, devel: true,
jquery: true, laxbreak: true
*/
/*globals mw: false, OO: false */
(function() {
"use strict";
var UPD_SCRIPT = "MediaWiki:Gadget-updates-core.js", /// "MediaWiki:Gadget-updates-core.js",
UPD_MAX_RETRIES = 5,
UPD_IMAGES_ENABLED = true,
UPD_ERR_REPORT_ENABLED = true,
UPD_ERR_REPORT_PAGE = UPD_SCRIPT.replace( ":", " talk:" ),
UPD_TITLE_FALSE_SUBPAGE = "false subpage",
UPD_LS_KEY = "gsw-updates",
UPD_HASH = "Gadget-updates";
var UPD = {
_initData: function() {
UPD.api = UPD.api || new mw.Api();
UPD.post = {
title: null,
restrictedTitleReasons: [],
url: null,
corsUrl: null,
qfc: null,
cgID: -1,
hasNavbar: false,
headersNum: 0,
headerSizes: {},
media: [],
imgsNum: 0,
vidsNum: 0,
mediaTitle: null,
hasHeader: false,
headerMediaType: "",
patchnotes: false,
content: null,
};
UPD.ooui = UPD.ooui || {
wm: OO.ui.getWindowManager(),
md: new OO.ui.MessageDialog(),
};
UPD.retries = 0;
UPD.$menu = UPD.$menu || null;
},
/**
* Gets a CORS-friendly variant of the given URL
*
* @param {string} url the URL to convert
* @returns {string|boolean} CORS-compliant URL if successful, false otherwise
*/
toCORS: function( url ) {
var temp;
try {
temp = new URL( url );
} catch( _ ) {
return false;
}
if( temp.hostname.search( /^((www|services|secure)\.)?gem\.com$/i ) === -1 ) {
return false;
}
// rs modules supported by wiki's /cors/ endpoint
if( temp.pathname.search( /^\/m=news\//i ) !== -1 ) {
return "https://chisel.weirdgloop.org/cors/m=" + temp.pathname.substring( 3 );
}
// all other rs modules
else {
//TODO: allorigins is not allowed by CSP; all other URLs will fail for now(?)
// will either need to add support to /cors/ for more modules (possible?) or modify CSP (seems unlikely)
///return "//api.allorigins.win/get?url=" + encodeURIComponent( temp.href ) + "&callback=?";
return false;
}
},
/**
* Converts QFC to URLs or removes these unnecessary/user-specific components from URLs:
* /c=.../ (for logged-in users)
* /sl=0/ (for logged-out users)
* /a=.../ (unknown - found in comment from previous script version)
* @param {string} url the URL/QFC to be cleaned
* @returns {string} the cleaned version of the URL
*/
checkURL: function( url ) {
if( url.search( /^([0-9]+[,-])+[0-9]+$/ ) !== -1 )
return "https://secure.gem.com/m=forum/forums?" + url.replace( /-/g, "," );
return url.replace( /\/(c|a|sl)=[^/]+\//gi, "/" );
},
/**
* Displays a status message using MediaWiki's notification API
* @param {string} msg message to display
* @param {string} [type] MediaWiki notification type
* @returns {void}
*/
statusMsg: function( msg, type ) {
type = type || "";
if( ["error", "warn", "info"].includes( type ) )
console[type]( msg );
else
console.log( msg );
mw.notify( msg, {
tag: "updates",
title: "Update Script",
type: type,
autoHide: false,
} );
},
/**
* Fetches a remote URL or QFC and begins the parsing of its contents
* @param {string} url the URL or QFC to fetch
* @param {boolean} retry false if this is the first attempt, true otherwise
* @returns {void}
*/
getPost: function( url, retry ) {
retry = retry || false;
debugger;
if( !retry ) {
UPD._initData();
UPD.post.url = UPD.checkURL( url );
UPD.post.corsUrl = UPD.toCORS( UPD.post.url );
if( !UPD.post.corsUrl ) {
OO.ui.alert( "Invalid URL or QFC given; please try again." );
$( "a", UPD.$menu ).click();
return;
}
UPD.statusMsg( "Fetching post data from " + UPD.post.url );
}
$.get( UPD.post.corsUrl, function( data ) {
UPD.retries = 0;
localStorage.removeItem( UPD_LS_KEY );
UPD.statusMsg( "Parsing data" );
UPD.parsePost( $( data ).find( "article.c-news-article__main, section.thread-view" ).first() );
} ).fail( function() {
if( ++UPD.retries <= UPD_MAX_RETRIES ) {
UPD.statusMsg( "Failed to fetch post, retrying... ( " + UPD.retries + " )" );
UPD.getPost( UPD.post.url, true );
} else {
OO.ui.alert( "Max retries exceeded, stopping. Check your connection, "
+ "ensure you are using a valid link/QFC, refresh, and try again." );
}
} );
},
/**
* Parse the retrieved data of an external news/forum post
* @param {jQuery} $post the external news/forum post as a jQuery object
* @returns {void}
*/
parsePost: function( $post ) {
var $content,
originalTitle = "",
divspancss = false;
// detect forum vs. news post, find $content container accordingly
var $threadTitle = $post.find( ".thread-view__heading" ),
isForumPost = $threadTitle.length !== 0,
$firstPost = isForumPost && $post.find( "article.forum-post:first" ) || null;
UPD.post.patchnotes = !isForumPost && $post.find( ".articleContent > h2" ).text().search( /patch notes/i ) !== -1;
if( isForumPost ) {
$content = $firstPost.find( ".forum-post__body" );
if( !$content.length ) {
UPD.statusMsg( "Unable to find post. Check the URL and try again.", "error" );
return;
}
if( $threadTitle.text().search( /patch notes/i ) === -1 ) {
UPD.statusMsg( "Forum thread does not appear to be about patch notes."
+ " Check the URL and try again.", "error" );
return;
} else {
UPD.post.patchnotes = true;
}
if( !$firstPost.hasClass( "forum-post--jmod" ) ) {
UPD.statusMsg( "Forum thread was not started by a JMod."
+ " Check the URL and try again.", "error" );
return;
}
} else {
$content = $post.find( "#article-content" );
if ( $content.length === 0 ) {
$content = $post.find( ".articleContentText" );
}
// console.log($content[0].innerText);
}
// meta info
if( UPD.post.patchnotes ) {
if( isForumPost ) {
var postTime = $firstPost.find( ".forum-post__time-below" ).text().substr( 0, 11 ).replace( /-/g, " " );
UPD.post.date = ( new Date( postTime ) ).toLocaleDateString( "en-GB", {
year: "numeric", month: "long", day: "numeric" } );
UPD.post.qfc = $post.find( ".thread-view__qfc-number" ).first().text();
} else {
UPD.post.date = $post.find( ".articleMeta time" ).text().replace( /\b0(?=\d)/g, "" );
}
UPD.post.title = "Patch Notes (" + UPD.post.date + ")";
} else {
UPD.post.title = $post.find( "#article-title" ).text().replace( /[‘’]/g, "\'" );
if( UPD.post.title.includes( "|" ) ) {
originalTitle = UPD.post.title.replace( /\|/g, "{{!}}" );
UPD.post.title = UPD.post.title.replace( /\|/g, "-" );
UPD.post.restrictedTitleReasons.push( "Cannot use pipe characters in titles" );
}
if( UPD.post.title.includes( "#" ) ) {
originalTitle = UPD.post.title;
UPD.post.title = UPD.post.title.replace( /#/g, "" );
UPD.post.restrictedTitleReasons.push( "Cannot use # in titles" );
}
if( UPD.post.title.includes( "/" ) ) {
UPD.post.restrictedTitleReasons.push( UPD_TITLE_FALSE_SUBPAGE );
}
if( UPD.post.title.charAt(0) !== UPD.post.title.charAt(0).toUpperCase() ) {
originalTitle = ( originalTitle ? originalTitle : UPD.post.title );
}
var colonPrefix = UPD.post.title.match( /^([^:]+): *(.*)/ );
if( colonPrefix && colonPrefix.length ) {
UPD.post.mediaTitle = colonPrefix[2];
//TODO: Is this necessary? Aren't we always prepending "Update:" to the title, forcing the namespace?
if( Object.values( mw.config.get( "wgFormattedNamespaces" ) ).includes( colonPrefix[1] ) ) {
originalTitle = UPD.post.title;
UPD.post.title = colonPrefix[1] + " - " + colonPrefix[2];
UPD.post.restrictedTitleReasons.push( "actual name begins with '" + colonPrefix[1]
+ ":', putting it in the wrong namespace; displaytitle used" );
}
} else {
UPD.post.mediaTitle = UPD.post.title;
}
UPD.post.mediaTitle = UPD.post.mediaTitle.charAt(0).toUpperCase() + UPD.post.mediaTitle.replace( /\//g, '-' ).slice(1);
UPD.post.date = $post.find( ".c-news-article__date" ).text().replace( /\b0(?=\d)/g, "" );
UPD.post.category = $post.find( ".c-news-article__category" ).html();
UPD.post.cgID = parseInt( $post.find( ".c-news-article__category" ).attr( "href" ).match( /cat=(\d+)/ )[1] );
}
if( UPD.post.patchnotes )
UPD.statusMsg( "Article detected as patch notes" );
//TODO: verify destination page name doesn't already exist
// if it does... OOUI input modal to request new page, loop until not exist or canceled?
/**
* Captures an image to be uploaded and returns wiki markup to display it
* @param {string} [url] remote URL of the image
* @param {string} [opts] extra options to pass in the [[File:...]] wiki markup
* @param {string} [linkText] non-empty value causes hyperlinked text to be inserted rather than the image
* @param {boolean} [isHeader] true if this is the header image, false otherwise
* @param {number} [width] width of the image
* @returns {string} the wiki markup to display the image or an empty string on failure
*/
function insertMedia( uncleanedUrl, opts, linkText, isHeader, width ) {
if(uncleanedUrl === undefined || uncleanedUrl === "") return "";
if(uncleanedUrl.match("https://cdn.gem.com/assets/img/external/news/Template_assets/scroll/")) {
console.log( "Ignoring style template assets: " + url);
return "";
}
var urlObj = new URL(uncleanedUrl);
var url = urlObj.origin + urlObj.pathname,
skip = false;
UPD.post.media.forEach( function(file) {
if ( file.url === url ) {
skip = true;
return;
}
});
if ( skip ) {
console.log( "Ignoring duplicate media URL: " + url );
return "";
}
opts = opts || "";
linkText = linkText || "";
isHeader = isHeader || false;
if( !UPD_IMAGES_ENABLED ) {
console.log( "Media disabled; ignoring media URL: " + url );
return "";
}
if( isHeader ) {
if( UPD.post.hasHeader ) {
UPD.statusMsg( "Post has multiple header media; ignoring all but the first...", "warn" );
return "";
} else {
UPD.post.hasHeader = true;
}
}
var imgExt = url.match( /\.(png|jpe?g|gif)$/i );
var vidExt = url.match( /\.(webm)$/i );
if( urlObj.host === "pbs.twimg.com" ) {
var twitterExt = urlObj.search.match( /format=(jpg|png)/i )
if( twitterExt !== null ) {
url += "." + twitterExt[1] + ":orig";
imgExt = twitterExt;
}
} else if(urlObj.host === "steamuserimages-a.akamaihd.net" ) {
url += "?output-format=jpg";
imgExt = [null,'jpg'];
} else if(urlObj.host === "preview.redd.it" ) {
url += urlObj.search;
}
var destName = UPD.post.mediaTitle + " ";
if( imgExt ) {
UPD.post.imgsNum += 1;
if( isHeader ) UPD.post.headerMediaType = 'image';
var imgNum = UPD.post.imgsNum + (UPD.post.headerMediaType === 'image' ? -1 : 0);
destName += ( isHeader ? "update header" : "(" + imgNum + ") update image" )
+ "." + imgExt[1].replace( "jpeg", "jpg" );
} else if( vidExt ) {
UPD.post.vidsNum += 1;
if( isHeader ) UPD.post.headerMediaType = 'video';
var vidNum = UPD.post.vidsNum + (UPD.post.headerMediaType === 'video' ? -1 : 0);
destName += ( isHeader ? "update header" : "(" + vidNum + ") update video" )
+ "." + vidExt[1];
} else {
UPD.statusMsg( "Unsupported media extension for URL: " + url );
return "";
}
UPD.post.media.push( {
url: url,
name: destName
} );
return "[[" + ( linkText ? ":" : "" ) + "File:" + destName
+ ( linkText ? "|" + linkText : ( opts ? "|" + opts : "" ) )
+ ( width >= 580 ? "|580px" : "" )
+ "|center]]";
}
// header image/video
// NOTE: ignored for patch notes to keep those wiki pages more uniform and compact
if( !UPD.post.patchnotes ) {
var $header = $post.find( ".c-news-article__figure iframe[src*='www.youtube.com']" ).first();
if( $header.length ) {
var embedURL = new URL( $header.attr( "src" ) );
$content.prepend( "{{Youtube|" + embedURL.pathname.match( /\/embed\/([^\/]+)/ )[1] + "}}\n" );
}
$header = $post.find( ".c-news-article__image" ).first();
if( $header.length ) {
var img = insertMedia( $header.attr( "src" ), "", "", true );
if( img ) {
$content.prepend( img + "\n\n" );
}
}
}
// ---------------------- begin HTML to wiki markup transformations ----------------------
// <style> <script> <meta> --> remove
$content.find( "style, script, meta" ).remove();
// remove useless divs
$content.find( "#article-top" ).replaceWith( function() {
return this.innerHTML;
} );
$content.find( ".category:not(details)" ).replaceWith( function() {
return this.innerHTML;
} );
$content.find( ".outro" ).replaceWith( function() {
return this.innerHTML;
} );
// If the newspost has a navbar, create level 1 headers for
// its sections
$content.find( ".news-tabs" ).parents( "nav" ).replaceWith( function() {
// remove parent NAV tag if it has one
return this.innerHTML;
});
var navbarLinks = {};
$content.find( ".news-tabs" ).replaceWith( function() {
UPD.post.hasNavbar = true;
$(this).find( "a" ).each( function() {
if ( !this.hash || !this.textContent ) return;
navbarLinks[this.hash] = this.textContent;
UPD.post.headersNum += 1;
});
return '';
} );
$content.find( ".news-tabs-content" ).replaceWith( function() {
const topHeaderText = navbarLinks['#' + this.id];
if ( !this.id || !topHeaderText ) return;
const topHeader = '{{UB|' + topHeaderText + '|header=1}}';
return topHeader + this.innerHTML;
} );
// Patch Notes styling
if( UPD.post.patchnotes ) {
var icons = [ 'achievements', 'mobile' ];
function getIconId( imgEl ) {
if ( !imgEl ) { return 'achievements'; }
var iconName = imgEl.src.match( /(\/([^\/]+?)\.png)$/ )[2];
if ( icons.indexOf( iconName.toLowerCase() ) !== -1 ) {
return iconName.toLowerCase();
} else {
return 'achievements';
}
}
// nav links
$content.find( ".legenda-item" ).replaceWith( function() {
var imgEl = $( this ).find( 'img' )[0];
return "{{UBLink|" + this.innerText + "|icons=" + getIconId(imgEl) + "}}";
});
// icon headers
$content.find( ".category-heading" ).replaceWith( function() {
var $header = $( this ).find( 'h1, h2, h3, h4, h5 ,h6' )[0],
imgEl = $( this ).find( 'img' )[0];
UPD.post.headersNum += 1;
var headerLevel = ( $header.style.fontSize === "1em" ? "3" : false );
return "\n{{UB|" + $header.innerText + ( headerLevel ? "|header=" + headerLevel : "" ) + "|icons=" + getIconId(imgEl) + "}}";
} );
// other headers
$content.find( "h4" ).replaceWith( function() {
UPD.post.headersNum += 1;
return "\n{{UB|" + this.innerText + "|header=3}}";
});
}
// remove empty tags
$content.find( "embed, link" ).replaceWith( function() {
if ( this.attributes.length === 0 && $(this).is(":empty") ) {
return "";
} else {
return this;
}
});
// collapsible sections
$content.find('details.category').replaceWith( function() {
// Summary with preserved italics and UB title
var $summary = $(this).find('summary').first()
$summary.find( "i, em" ).replaceWith( function() {
return "''" + this.innerHTML + "''";
} );
var summaryText = $summary.text().trim().replace('|', '<nowiki>|</nowiki>');
// Use UB template
var summaryTitle = summaryText.split(/''|<nowiki>/)[0].replace(/\n$/, '');
summaryText = summaryText.replace(summaryTitle, '{{UB|'+summaryTitle+'}}')
// Header image?
if ( $(this).find('summary').first().css('background-image').indexOf('url') >= 0 ) {
var imgurl = $(this).find('summary').first().css('background-image').replace(/^url\("|"\)$/g, '')
summaryText += '\n' + insertMedia( imgurl, "", "", false, 800 );
}
// Content
var htmlstr = "";
$(this).children(':not(summary)').each( function(){
htmlstr += this.outerHTML;
});
return "<div class=\"mw-collapsible mw-collapsed rs-update-section\">\n<div>\n" + summaryText + "\n</div>\n<div class=\"mw-collapsible-content\">\n" + htmlstr + "\n</div>\n</div>"
})
$content.find('details').replaceWith( function() {
var summaryText = $(this).find('summary')[0].textContent.trim().replace('|', '<nowiki>|</nowiki>');
var htmlstr = "";
$(this).children(':not(summary)').each( function(){
htmlstr += this.outerHTML;
});
return "{| class=\"mw-collapsible mw-collapsed\"\n! " + summaryText + "\n|-\n| " + htmlstr + "\n|}"
})
// replace artstation gallery container with plain file links
$content.find('div.container > .mySlides').parent().replaceWith( function() {
return $content.find('div.container > .mySlides').contents();
});
// first div, if it contains a numbered list --> remove (we have TOCs for that)
{
var $firstChild = $content.children().first();
if( $firstChild.prop( "tagName" ) === "DIV" && $firstChild.find( "> ol" ).length ) {
$firstChild.remove();
}
}
// YouTube videos --> {{Youtube}}
$content.find( "iframe[src*='www.youtube.com']" ).replaceWith( function() {
return "{{Youtube|" + this.src.match( /\/embed\/([^\/?&]+)/ )[1] + "}}\n";
} );
// all other <iframe>s --> centred hyperlinks
$content.find( "iframe" ).replaceWith( function() {
var srcUrl = new URL( this.src, UPD.post.url );
return "<div style=\"text-align: center;\">" + srcUrl.href + "</div>";
} );
// Reddit post embed styling
$content.find( ".reddit-card" ).replaceWith( function() {
return '<div class="news-embed reddit">' + this.innerHTML + '\n</div>';
} );
// Tweet embed styling
$content.find( ".twitter-tweet" ).replaceWith( function() {
$(this).find( "p" ).replaceWith( function() {
return this.innerHTML + "\n\n";
} );
return '<div class="news-embed twitter">' + this.innerHTML + '\n</div>';
} );
// <p> <blockquote> --> line breaks
// remove empty elements
$content.find( "p, blockquote" ).replaceWith( function() {
if ( this.innerHTML.trim() !== "" ) {
return "\n\n" + this.innerHTML;
}
else {
return "";
}
} );
// <q> --> inline quotations
//TODO: would like to see an example of an update that uses this; might be unused in newer format
$content.find( "q" ).replaceWith( function() {
return "''\"" + this.innerText + "\"''";
} );
// remove HTML comments
$content.contents().filter( function() {
return this.nodeType === Node.COMMENT_NODE;
} ).remove();
// get header font sizes and assign them a standard header level
$content.find( "h1, h2, h3, h4, h5, h6" ).each( function() {
var fsize;
switch( this.style.fontSize ) {
case "":
fsize = 2;
break;
case "1.25em":
fsize = 3;
break;
case "1em":
fsize = 4;
break;
default:
fsize = 0;
}
UPD.post.headerSizes[this.style.fontSize] = fsize;
} );
// transform header level dynamically based on the headers present
var hvalues = Object.values(UPD.post.headerSizes).sort();
var newhsizes = {};
for (var k in UPD.post.headerSizes) {
newhsizes[k] = hvalues.indexOf(UPD.post.headerSizes[k]) + 2;
}
// headers --> {{UB}}
$content.find( "h1, h2, h3, h4, h5, h6" ).replaceWith( function() {
UPD.post.headersNum += 1;
var headerLevel = newhsizes[this.style.fontSize] || false;
if (headerLevel === 2) headerLevel = false;
return "\n{{UB|" + this.innerText + ( headerLevel ? "|header=" + headerLevel : "" ) + "}}";
} );
// headers nested in collabsible sections
$content.find( "div.mw-collapsible.rs-update-section .mw-collapsible-content" ).replaceWith( function() {
var htmlstr = this.outerHTML;
htmlstr = htmlstr.replace(/(\|header=)(\d+)(}})/g, function($1,$2,$3,$4) {
return $2 + (parseInt($3)+1) + $4;
} );
htmlstr = htmlstr.replace( /({{UB\|[^\|}]+)(}})/g, '$1|header=3$2' );
return htmlstr;
} );
// bold text --> '''bold text'''
$content.find( "b, strong, span[style*='trajan-pro-3']" ).replaceWith( function() {
return "'''" + this.innerHTML + "'''";
} );
// italic text --> ''italic text''
$content.find( "i, em" ).replaceWith( function() {
return "''" + this.innerHTML + "''";
} );
// <font> --> {{Colour}}
$content.find( "font" ).replaceWith( function () {
if ( this.color ) {
return "{{Colour|#" + this.color + "|" + this.innerHTML + "}}";
} else {
return this.innerHTML;
}
} );
// horizontal lines --> ----
$content.find( "hr" ).replaceWith( "\n----\n" );
// unordered list items --> * list item
// weird pointless ul nesting
$content.find( "ul" ).each( function () {
var $ul = $( this );
if ( $ul.children(":first").is("ul") && $ul.html().trim() == $ul.children(":first")[0].outerHTML.trim() ) {
$ul.replaceWith( $ul.children(":first") );
}
} );
// weird ul ul ul li
$content.find( "ul ul ul li" ).replaceWith( function() {
return "*** " + this.innerHTML;
} );
// weird ul ul li
$content.find( "li ul li, ul ul li" ).replaceWith( function() {
return "** " + this.innerHTML;
} );
//TODO: does this work with multiple levels? answer: no
$content.find( "ul li" ).replaceWith( function() {
return "* " + this.innerHTML;
} );
// ordered list items --> # list item
$content.find( "li ol li" ).replaceWith( function() {
return "## " + this.innerHTML;
} );
//TODO: does this work with multiple levels?
$content.find( "ol li" ).replaceWith( function() {
return "# " + this.innerHTML;
} );
// remaining list items (likely invalid HTML) --> * list item
$content.find( "li" ).replaceWith( function() {
return "* " + this.innerHTML;
} );
// (un)ordered list elements --> their already-parsed contents
$content.find( "ul, ol" ).replaceWith( function() {
return '\n' + this.innerHTML;
} );
// handle weird ul ul and ul ul ul lists
$content.find( "ul" ).replaceWith( function() {
return this.innerHTML;
} );
$content.find( "ul" ).replaceWith( function() {
return this.innerHTML;
} );
// Fix line breaks in lists
var contStr = $content.html();
contStr = contStr.replace(/[\r\n]{2,}\*/g, '\n*');
$content.html( contStr );
$content.find( "u" ).replaceWith( function() {
return '<span style="text-decoration: underline;">' + this.innerHTML + '</span>';
} );
$content.find( "span, div" ).each( function() {
var tag = this.tagName.toLowerCase();
// keep tags that have inline styles, else strip the tags
if( this.hasAttribute( "style" ) ) {
divspancss = true;
return '<' + tag + ' style="' + this.getAttribute( 'style' ) + '">'
+ ( this.innerHTML + ' ' ).trim() + '</' + tag + '>';
} else {
return this.innerHTML;
}
} );
$content.find( "center, .feedback" ).replaceWith( function() {
return '<div style="text-align: center;">' + this.innerHTML + "</div>";
} );
//TODO: forum posts need these stripped if direct descendent of post container
//TODO: maybe try selecting repeated siblings and preserving those before removing the others?
$content.find( "br" ).replaceWith( "<br>" );
// links to images
$content.find( "a[href*='://services.gem.com/'], a[href*='://cdn.gem.com/']" ).filter( function() {
return ( this.href.search( /\.(jpe?g|png|gif|webm)$/ ) !== -1 );
} ).replaceWith( function() {
return insertMedia( this.href, "", this.innerText );
} );
// other links
$content.find( "a" ).replaceWith( function() {
var hrefAttr = this.getAttribute( "href" ),
trailSpace = (this.innerText.slice(-1) === ' ' ? ' ' : ''),
linkText = this.innerText.trim(),
url;
try { url = new URL(hrefAttr); } catch (_) {}
if( hrefAttr && hrefAttr[0] === "#" && this.innerText.includes( "Top" ) )
return "{{top}}";
else if( url && url.host === "gem.wiki" && url.pathname.startsWith("/w/") ) {
var wikiLink = url.pathname.replace('/w/', '').replaceAll('_', ' ').trim();
wikiLink = ((wikiLink.charAt(0).toLowerCase() + wikiLink.slice(1)) === linkText ? (wikiLink.charAt(0).toLowerCase() + wikiLink.slice(1)) : wikiLink);
return "{{WUL|" + wikiLink + ( wikiLink !== linkText ? "|" + linkText : "" ) + "}}" + trailSpace;
} else
return "[" + UPD.checkURL( this.href ) + " " + linkText + "]" + trailSpace;
} );
// images
$content.find( "img" ).replaceWith( function() {
return insertMedia( this.src, "", "", false, this.width );
} );
// videos
$content.find( "video" ).replaceWith( function() {
return insertMedia( this.getElementsByTagName('source')[0].src );
} );
// tables
$content.find( "td" ).replaceWith( function() {
const colspan = (this.colSpan > 1 ? 'colspan="' + this.colSpan + '" ' : '');
const rowspan = (this.rowSpan > 1 ? 'rowspan="' + this.rowSpan + '" ' : '');
const cellspan = ( colspan + rowspan !== '' ? colspan + rowspan + '| ' : '' );
return "| " + cellspan + this.innerHTML;
} );
$content.find( "th" ).replaceWith( function() {
const colspan = (this.colSpan > 1 ? 'colspan="' + this.colSpan + '" ' : '');
const rowspan = (this.rowSpan > 1 ? 'rowspan="' + this.rowSpan + '" ' : '');
const cellspan = ( colspan + rowspan !== '' ? colspan + rowspan + '| ' : '' );
return "! " + cellspan + this.innerHTML;
} );
$content.find( "tr" ).replaceWith( function() {
return "|-" + this.innerHTML.trimEnd();
} );
$content.find( "tbody" ).replaceWith( function() {
return this.innerHTML;
} );
$content.find( "table" ).replaceWith( function() {
return '{| class="wikitable"' + this.innerHTML + '|}';
} );
// ---------------------- end HTML to wiki markup transformations ----------------------
var htmlEncoding = {
lt:"<",
gt:">",
quot:"\"", rdquo:"\"", ldquo: "\"",
apos:"'", rsquo:"'", lsquo: "'",
hellip:"…",
ndash:"–"
},
htmlRegexp = new RegExp( "&(" + Object.keys( htmlEncoding ).join( "|" ) + ");", "g" );
var content = $content.html()
.replace( /\[\[:File:([^|]*)\|\s*\[\[File:([^|]*)\]\]\s*\]\]/gi, "[[File:$2|link=File:$1|center]]" )
// TODO: what?
.replace( htmlRegexp, function( match, type ) { return htmlEncoding[type]; } )
// decode certain HTML entities
.replace( / ?<br> ?<br>/g, "\n\n" ) // replace double-br with two newlines
.replace( / ?<br> ?/g, "\n" ) // replace single brs with just a newline
.replace( /[“”]/g, '"' ) // replace curly double quotes with standard ones
.replace( /[‘’]/g, "'" ) // repalce curly single quotes with standard ones
.replace( /\n{3,}/g, "\n\n" ) // fixing 3x+ enters caused by 2 paragraphs below each other
.replace( /&/g, "&" ) // replace "&" appearing in links and headers with just "&"
.replace( /[ \t]+$/gm, "" ) // trim excess whitespace from the end of a line
.replace( /^[ \t]+/gm, "" ) // trim excess whitespace from the beginning of a line
.replace( / {2,}/g, " " ) // condense spaces
;
if( UPD.post.patchnotes ) {
content = "{{Patch Notes"
+ "|date=" + UPD.post.date
+ "|qfc=" + ( UPD.post.qfc ? UPD.post.qfc : "|url=" + UPD.post.url )
+ "|notoc=yes}}\n\n" + content;
} else {
// console.log(UPD.post.headersNum);
content = "{{Update"
+ "|date=" + UPD.post.date
+ "|category=" + UPD.post.category
+ (UPD.post.headersNum >= 4 ? "|toc=t" : "")
+ (UPD.post.hasNavbar ? "|toclimit=2" : "")
+ "|link=" + UPD.post.url
+ ( originalTitle ? "|title=Update:" + originalTitle : "" )
+ "}}\n\n" + content;
}
if( UPD.post.restrictedTitleReasons.length ) {
content += "\n{{Restricted title"
+ "|" + UPD.post.restrictedTitleReasons.join( ", " )
+ ( UPD.post.restrictedTitleReasons.includes( UPD_TITLE_FALSE_SUBPAGE ) ? "|subpage=1" : "" )
+ "}}";
}
if( divspancss ) {
UPD.statusMsg( "Some inline CSS in a span/div tag was detected and brought in. "
+ "Check that it looks OK - text/background colour, borders, etc." );
}
UPD.post.content = content;
console.log(content);
// verification that everything has been parsed
var allowedTags = ["div", "span", "sub", "sup", "nowiki"],
$nonTextNodes = $content.contents().filter( function() {
return this.nodeType !== Node.TEXT_NODE && !allowedTags.includes( this.tagName.toLowerCase() );
} );
if( $nonTextNodes.length ) {
var unparsed = [];
$nonTextNodes.each( function() {
unparsed.push( "<" + this.tagName + "> of nodeType=" + this.nodeType );
} );
UPD.statusMsg( "Warning: Some HTML tags found in the remote post have not been parsed. "
+ "This might not have any consequences, but please do verify every edit made is correct.<br><br>"
+ "Debug info:<br><ul>"
+ "<li> " + unparsed.join( "</li>\n<li> " ) + "</li></ul>\n"
+ ( UPD_ERR_REPORT_ENABLED ? "<br>An error report is being sent for further review." : "" ) );
// if error reporting is enabled, call the report handler (which will then start the upload process)
// otherwise, start the upload process now
if( UPD_ERR_REPORT_ENABLED )
UPD.errorReport( unparsed );
else
UPD.uploadFiles();
} else {
// no unhandled non-text nodes found; start upload process now
UPD.uploadFiles();
}
},
/**
* Uploads media used in the post (if any) then begins the page creation
* @returns {void}
*/
uploadFiles: function() {
if( !UPD.post.media.length )
return UPD.previewPage();
UPD.ooui.wm.openWindow( UPD.ooui.md, {
title: "Upload media?",
message: "Images and/or videos were found in the post. Would you like to upload them automatically?",
actions: [
{
action: "yes",
label: "Yes",
},
{
action: "no",
label: "No",
},
{
label: "Cancel",
},
],
} )
.closing.done( function( confirmed ) {
if( confirmed )
confirmed = confirmed.action;
if( confirmed === "yes" ) {
var comment = "Automatically uploading news post image for [[Update:" + UPD.post.title + "]] "
+ "using [[" + UPD_SCRIPT + "|updates script]].";
// use a recursive local function to upload media synchronously.
// looping this way prevents overloading
( function uploadFile( i, opts ) {
UPD.statusMsg( "Uploading file #" + ( i + 1 ) + "/" + UPD.post.media.length );
var postreq = {
action: "upload",
filename: UPD.post.media[i].name,
comment: comment + "\nSource: " + UPD.post.media[i].url,
text: "{{Update image|" + UPD.post.title + "|link=" + UPD.post.media[i].url + "}}",
ignorewarnings: "true",
url: UPD.post.media[i].url,
}
if ( opts && opts.mismatch ) {
if ( opts.mismatch[2] === "image/jpeg" ) {
var newExt = ".jpg";
postreq.filename = postreq.filename.replace(/(\.[A-z]+)$/, newExt);
} else {
console.log( "MIME type: " + opts.mismatch[2] + " is not supported.");
}
}
UPD.api.postWithToken( "csrf", postreq ).done( function() {
//TODO: does the Deferred obj resolve even if errors occur?
// if so, might need to parse response to detect errors
// recurse if more images remain; else start page creation process
if( UPD.post.media[i + 1] )
uploadFile( i + 1 );
else
UPD.previewPage();
} ).fail( function( textStatus, jqXHR ) {
//TODO: verify params (induce an error elsewhere and see how mw API responds?)
if ( textStatus === "fileexists-no-change") {
UPD.statusMsg( "Skipping file #" + ( i + 1 ) + "/" + UPD.post.media.length
+ "; already exists and has not changed." );
if( UPD.post.media[i + 1] )
uploadFile( i + 1 );
else
UPD.previewPage();
} else if ( textStatus === "verification-error" && jqXHR.error.details[0] === "filetype-mime-mismatch" ) {
UPD.statusMsg( jqXHR.error.info + " uploading file #" + ( i + 1 ) + "/" + UPD.post.media.length
+ " to appropriate extension." );
uploadFile( i, { mismatch: jqXHR.error.details } );
} else {
UPD.statusMsg( "Uploading failed for media URL: " + UPD.post.media[i].url + "\n"
+ "API error: " + textStatus, "warn" );
}
} );
} )( 0 ); //initiate first upload
} else if( confirmed === "no" ) {
// user does not want media uploaded automatically; skip to page creation
return UPD.previewPage();
} else {
// user canceled request; do nothing
}
} );
},
/**
* Saves new page's contents to localStorage and redirects user to a preview
* NOTE: This ends the initial chain of the script; preview page's entry point is UPD.loadPreview()
* @returns {void}
*/
previewPage: function() {
window.localStorage.setItem( UPD_LS_KEY, UPD.post.content );
UPD.statusMsg( "Generating new page preview, redirecting..." );
window.location.href = "/w/Update:" + encodeURIComponent( UPD.post.title ) + "?action=edit#" + UPD_HASH;
},
/**
* Entry point for creation page; loads contents from localStorage and submits preview
* @returns {void}
*/
loadPreview: function() {
var $ef = $( "#editform" ),
action = $ef.attr( "action" ),
$ta = $( "textarea:eq(0)", $ef ),
$summ = $( "#wpSummary" ),
$prev = $( "#wpPreview" ),
content = window.localStorage.getItem( UPD_LS_KEY );
if( !content ) {
UPD.statusMsg( "Could not find new page contents in localStorage. Oops!", "error" );
debugger;
return;
}
if( !$ta.length || !$prev.length ) {
UPD.statusMsg( "Can't find editor page elements. Oops!", "error" );
debugger;
return;
}
localStorage.removeItem( UPD_LS_KEY );
$ta.val( content );
$summ.val( "Automated creation using [[" + UPD_SCRIPT + "|updates script]]." );
$ef.attr( "action", action + "#" + UPD_HASH );
$prev.click();
},
hookSubmit: function() {
//TODO: new entry point for action=submit page, hook submit button and call UPD.purgePages() on click
},
purgePages: function() {
UPD.statusMsg( "Page editing finished. Preparing to purge indexes..." );
var d = new Date( UPD.post.date ),
pagesToPurge = [];
pagesToPurge.push( d.getFullYear() );
pagesToPurge.push( d.toLocaleDateString( "en-GB", { day: "numeric", month: "long" } ) );
pagesToPurge.push( "Template:Updates" );
pagesToPurge.push( "Gem Wiki" );
switch( UPD.post.cgID ) {
case 1: pagesToPurge.push( "Game updates" ); break;
case 2: pagesToPurge.push( "Website updates" ); break;
case 3: pagesToPurge.push( "Customer Support updates" ); break;
case 4: pagesToPurge.push( "Technical updates" ); break;
case 5: pagesToPurge.push( "Community updates" ); break;
case 6: pagesToPurge.push( "Behind the Scenes" ); break;
case 9: pagesToPurge.push( "Shop updates" ); break;
case 12: pagesToPurge.push( "Future updates" ); break;
case 13: pagesToPurge.push( "Solomon's Store updates" ); break;
case 14: pagesToPurge.push( "Treasure Hunter updates" ); break;
case 15: pagesToPurge.push( "Feedback updates" ); break;
case 16: pagesToPurge.push( "Event updates" ); break;
}
//TODO: this was a null edit previously. why? isn't a purge enough?
// and if not, will specifying `forcerecursivelinkupdate: true` work?
if( pagesToPurge.length ) {
UPD.api.post( {
format: "json",
action: "purge",
titles: pagesToPurge.join( "|" ),
} )
.done( function( data ) {
// noinspection JSUnresolvedVariable - API response, too lazy to document it
data.purge.forEach( function(page) {
if( "purged" in page ) {
UPD.statusMsg( "Purge of " + page.title + " successful!" );
} else {
var reason;
if( "missing" in page )
reason = "Page appears to be missing";
else if( "invalidreason" in page )
reason = page.invalidreason;
else
reason = "Unknown reason";
UPD.statusMsg( "Failed to purge " + page.title + ": " + reason
+ '\n<br>Please manually purge this page: <a href="/w/' + page.title + '" target="_blank">here</a>' );
}
});
} )
.fail( function( _, data ) {
var msg = "Failed to purge pages, API returned error: " + data.error.info
+ "\n<br>Please manually purge these pages: "
+ "\n<br><ul>\n";
pagesToPurge.forEach( function(page) {
msg += '<li><a href="/w/' + page + '" target="_blank">' + page + '</a></li>\n';
});
msg += "</ul>";
UPD.statusMsg( msg, "error" );
} );
}
return UPD.success();
},
success: function() {
UPD.statusMsg( "Updating complete!"
+ '\n<br>Click <a href="/w/Update:' + UPD.post.title + '" target="_blank">here</a> to view the new post.' );
},
errorReport: function(unparsed) {
var message = "While updating the page [[Update:" + UPD.post.title + "]] "
+ "the following tag(s) occurred which were not parsed correctly:\n"
+ "<pre>" + unparsed.join( "\n" ) + "</pre>\n~~" + "~~";
//<pre> -- might work
//-----------------------------------------------------------------------------------------------
//TODO: remove after testing
var DEBUG_HALT = false;
var DEBUG_STUB = false;
if( DEBUG_HALT ) {
console.log( "DEBUG_HALT is true, will not actually submit error report..." );
return;
} else if( DEBUG_STUB ) {
console.log( "DEBUG_STUB is true, pretending report was submitted successfully..." );
return UPD.uploadFiles();
}
//-----------------------------------------------------------------------------------------------
UPD.api.postWithToken( "csrf", {
action: "edit",
section: "new",
sectiontitle: "Automated error report for " + UPD.post.title,
title: UPD_ERR_REPORT_PAGE,
summary: "Error report for [[" + UPD_SCRIPT + "|updates script]].",
text: message,
notminor: true,
} )
.done( UPD.uploadFiles )
.fail( function( _, data ) {
UPD.statusMsg( "Failed to submit error report, API error: " + data.error.info, "error" );
OO.ui.confirm( "Failed to submit error report. Something is seriously broken. "
+ "Please note errors in the status area and manually report them on [[" + UPD_ERR_REPORT_PAGE + "]]. "
+ "Press OK to try to continue with the post anyway." )
.done( function( confirmed ) {
if( confirmed )
UPD.uploadFiles();
else
UPD.statusMsg( "Post aborted." );
} );
} );
},
init: function() {
// init internal data
UPD._initData();
// add our OOUI MessageDialog to the WindowManager
//noinspection JSCheckFunctionSignatures - matches example in source docs
UPD.ooui.wm.addWindows( [ UPD.ooui.md ] );
// add button to vector skin
UPD.$menu = $( mw.util.addPortletLink(
"p-views",
"#",
"Create update",
"ca-update",
"Creates a new update"
) )
.on( "click", function() {
new OO.ui.prompt( "Enter update page URL or forum QFC", {
textInput: {
placeholder: "http://geministation.com/patch-notes",
},
} )
.done( function ( result ) {
result = result.trim();
if ( result.length ) {
UPD.statusMsg( "Started the update script." );
UPD.getPost( result );
}
} );
} );
}
};
// save UPD object in window object for external references
window.UPD = UPD;
} )();
// </nowiki>