// ==UserScript==
// @name Instant-Cquotes
// @name:it Instant-Cquotes
// @version 0.34
// @description Automatically converts selected FlightGear mailing list and forum quotes into post-processed MediaWiki markup (i.e. cquotes).
// @description:it Converte automaticamente citazioni dalla mailing list e dal forum di FlightGear in marcatori MediaWiki (cquote).
// @author Hooray, bigstones, Philosopher, Red Leader & Elgaton (2013-2016)
// @icon http://wiki.flightgear.org/images/2/25/Quotes-logo-200x200.png
// @match https://sourceforge.net/p/flightgear/mailman/*
// @match http://sourceforge.net/p/flightgear/mailman/*
// @match https://forum.flightgear.org/*
// @match http://wiki.flightgear.org/*
// @namespace http://wiki.flightgear.org/FlightGear_wiki:Instant-Cquotes
// @run-at document-end
// @require https://code.jquery.com/jquery-1.10.2.js
// @require https://code.jquery.com/ui/1.11.4/jquery-ui.js
// @resource jQUI_CSS https://code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css
// @resource myLogo http://wiki.flightgear.org/images/2/25/Quotes-logo-200x200.png
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_getResourceText
// @grant GM_getResourceURL
// @grant GM_setClipboard
// @grant GM_xmlhttpRequest
// @noframes
// ==/UserScript==
//
// This work has been released into the public domain by their authors. This
// applies worldwide.
// In some countries this may not be legally possible; if so:
// The authors grant anyone the right to use this work for any purpose, without
// any conditions, unless such conditions are required by law.
//
'use strict';
// TODO: move to GreaseMonkey host
// prevent conflicts with jQuery used on webpages: https://wiki.greasespot.net/Third-Party_Libraries#jQuery
this.$ = this.jQuery = jQuery.noConflict(true);
// set this to true continue working on the new mode supporting
// asynchronous content fetching via AJAX
// var USE_NG = false;
// This hash is intended to help encapsulate platform specifics (browser/scripting host)
// Ideally, all APIs that are platform specific should be kept here
// This should make it much easier to update/port and maintain the script in the future
var Environment = {
getHost: function() {
// This will determine the script engine in use: http://stackoverflow.com/questions/27487828/how-to-detect-if-a-userscript-is-installed-from-the-chrome-store
if (typeof(GM_info) === 'undefined') {
Environment.scriptEngine = "plain Chrome (Or Opera, or scriptish, or Safari, or rarer)";
// See http://stackoverflow.com/a/2401861/331508 for optional browser sniffing code.
}
else {
Environment.scriptEngine = GM_info.scriptHandler || "Greasemonkey";
}
console.log ('Instant cquotes is running on ' + Environment.scriptEngine + '.');
// See also: https://wiki.greasespot.net/Cross-browser_userscripting
return Environment.GreaseMonkey; // return the only/default host (for now)
},
validate: function(host) {
if(Environment.scriptEngine !== "Greasemonkey" && host.get_persistent('startup.disable_validation',false)!==true)
alert("NOTE: This script has not been tested with script engines other than GreaseMonkey recently!");
},
// this contains unit tests for checking crucial APIs that must work for the script to work correctly
// for the time being, most of these are stubs waiting to be filled in
// for a working example, refer to the JSON test at the end
// TODO: add jQuery tests
APITests: [
{name:'download', test: function(recipient) {recipient(true);} },
{name:'make_doc', test: function(recipient) { recipient(true);} },
{name:'eval_xpath', test: function(recipient) { recipient(true);} },
{name:'JSON de/serialization', test: function(recipient) {
//console.log("running json test");
var identifier = 'unit_tests.json_serialization';
var hash1 = {x:1,y:2,z:3};
Host.set_persistent(identifier, hash1, true);
var hash2 = Host.get_persistent(identifier,null,true);
recipient(JSON.stringify(hash1) === JSON.stringify(hash2));
} // callback
},
// downloads a posting and tries to transform it to 3rd person speech ...
// TODO: add another test to check forum postings
{name:'text/speech transformation', test: function(recipient) {
// the posting we want to download
var url='https://sourceforge.net/p/flightgear/mailman/message/35066974/';
Host.downloadPosting(url, function (result) {
// only process the first sentence by using comma/dot as delimiter
var firstSentence = result.posting.substring(result.posting.indexOf(',')+1, result.posting.indexOf('.'));
var transformed = transformSpeech(firstSentence, result.author, null, speechTransformations );
console.log("3rd person speech transformation:\n"+transformed);
recipient(true);
}); // downloadPosting()
}// test()
} // end of speech transform test
], // end of APITests
runAPITests: function(host, recipient) {
console.log("Running API tests");
for(let test of Environment.APITests ) {
//var test = Environment.APITests[t];
// invoke the callback passed, with the hash containing the test specs, so that the console/log or a div can be updated showing the test results
//callback(test);
recipient.call(undefined, test);
//test.call(undefined, callback)
} // foreach test
}, // runAPITests
///////////////////////////////////////
// supported script engines:
///////////////////////////////////////
GreaseMonkey: {
// TODO: move environment specific initialization code here
init: function() {
// Check if Greasemonkey/Tampermonkey is available
try {
// TODO: add version check for clipboard API and check for TamperMonkey/Scriptish equivalents ?
GM_addStyle(GM_getResourceText('jQUI_CSS'));
} // try
catch (error) {
console.log('Could not add style or determine script version');
} // catch
var commands = [
{name:'Setup quotes',callback:setupDialog, hook:'S' },
{name:'Check quotes',callback:selfCheckDialog, hook:'C' }
];
for (let c of commands ) {
this.registerMenuCommand(c.name, c.callback, c.hook);
}
}, // init()
getScriptVersion: function() {
return GM_info.script.version;
},
dbLog: function (message) {
if (Boolean(DEBUG)) {
console.log('Instant cquotes:' + message);
}
}, // dbLog()
registerMenuCommand: function(name,callback,hook) {
// https://wiki.greasespot.net/GM_registerMenuCommand
// https://wiki.greasespot.net/Greasemonkey_Manual:Monkey_Menu#The_Menu
GM_registerMenuCommand(name, callback, hook);
}, //registerMenuCommand()
download: function (url, callback, method='GET') {
// http://wiki.greasespot.net/GM_xmlhttpRequest
try {
GM_xmlhttpRequest({
method: method,
url: url,
onload: callback
});
}catch(e) {
console.log("download did not work");
}
}, // download()
// is only intended to work with archives supported by the hash
downloadPosting: function (url, EventHandler) {
Host.download(url, function (response) {
var profile = getProfile(url);
var blob = response.responseText;
var doc = Host.make_doc(blob,'text/html');
var xpath_author = '//'+profile.author.xpath;
var author = Host.eval_xpath(doc, xpath_author).stringValue;
author = profile.author.transform(author);
var xpath_date = '//' + profile.date.xpath;
var date = Host.eval_xpath(doc, xpath_date).stringValue;
date = profile.date.transform(date);
var xpath_posting = '//'+profile.content.xpath;
var posting = Host.eval_xpath(doc, xpath_posting).stringValue;
var result = {author:author, date:date, posting:posting};
EventHandler(result);
}); // AJAX callback
}, // downloadPosting()
// turn a string/text blob into a DOM tree that can be queried (e.g. for xpath expressions)
// FIXME: this is browser specific not GM specific ...
make_doc: function(text, type='text/html') {
// to support other browsers, see: https://developer.mozilla.org/en/docs/Web/API/DOMParser
return new DOMParser().parseFromString(text,type);
}, // make DOM document
// xpath handling may be handled separately depending on browser/platform, so better encapsulate this
// FIXME: this is browser specific not GM specific ...
eval_xpath: function(doc, xpath, type=XPathResult.STRING_TYPE) {
return doc.evaluate(xpath, doc, null, type, null);
}, // eval_xpath
set_persistent: function(key, value, json=false)
{
// transparently stringify to json
if(json) {
// http://stackoverflow.com/questions/16682150/store-a-persistent-list-between-sessions
value = JSON.stringify (value);
}
// https://wiki.greasespot.net/GM_setValue
GM_setValue(key, value);
}, // set_persistent
get_persistent: function(key, default_value, json=false) {
// https://wiki.greasespot.net/GM_getValue
var value=GM_getValue(key, default_value);
// transparently support JSON: http://stackoverflow.com/questions/16682150/store-a-persistent-list-between-sessions
if(json) {
value = JSON.parse (value) || {};
}
return value;
}, // get_persistent
setClipboard: function(msg) {
// this being a greasemonkey user-script, we are not
// subject to usual browser restrictions
// http://wiki.greasespot.net/GM_setClipboard
GM_setClipboard(msg);
} // setClipboard()
} // end of GreaseMonkey environment, add other environments below
}; // Environment hash - intended to help encapsulate host specific stuff (APIs)
// the first thing we need to do is to determine what APIs are available
// and store everything in a Host hash, which is used for API lookups
// the Host hash contains all platform/browser-specific APIs
var Host = Environment.getHost();
Host.init(); // run environment specific initialization code (e.g. logic for GreaseMonkey setup)
// move DEBUG handling to a persistent configuration flag so that we can configure this using a jQuery dialog (defaulted to false)
// TODO: move DEBUG variable to Environment hash / init() routine
var DEBUG = Host.get_persistent('debug_mode_enabled', false);
Host.dbLog("Debug mode is:"+DEBUG);
function DEBUG_mode() {
// reset script invocation counter for testing purposes
Host.dbLog('Resetting script invocation counter');
Host.set_persistent(GM_info.script.version, 0);
}
if (DEBUG)
DEBUG_mode();
// downloadOptionsXML();
// hash with supported websites/URLs, includes xpath and regex expressions to extract certain fields, and optional transformations for post-processing
var CONFIG = {
// WIP: the first entry is special, i.e. it's not an actual list archive (source), but only added here so that the same script can be used
// for editing the FlightGear wiki
'FlightGear wiki': {
type: 'wiki',
enabled: false,
event: 'document.onmouseup', // when to invoke the event handler
event_handler: function () {
console.log('FlightGear wiki handler active (waiting to be populated)');
// this is where the logic for a wiki mode can be added over time (for now, it's a NOP)
var editSections = document.getElementsByClassName('mw-editsection');
console.log('FlightGear wiki article, number of edit sections: '+editSections.length);
// for now, just rewrite edit sections and add a note to them
[].forEach.call(editSections, function (sec) {
sec.appendChild(
document.createTextNode(' (instant-cquotes is lurking) ')
);
}); //forEach section
}, // the event handler to be invoked
url_reg: '^(http|https)://wiki.flightgear.org' // ignore: not currently used by the wiki mode
}, // end of wiki profile
'Sourceforge Mailing list': {
enabled: true,
type: 'archive',
event: 'document.onmouseup', // when to invoke the event handler
event_handler: instantCquote, // the event handler to be invoked
url_reg: '^(http|https)://sourceforge.net/p/flightgear/mailman/.*/',
content: {
xpath: 'tbody/tr[2]/td/pre/text()', // NOTE this is only used by the downloadPosting helper to retrieve the posting without having a selection
selection: getSelectedText,
idStyle: /msg[0-9]{8}/,
parentTag: [
'tagName',
'PRE'
]
}, // content recipe
// vector with tests to be executed for sanity checks (unit testing)
tests: [
{
url: 'https://sourceforge.net/p/flightgear/mailman/message/35059454/',
author: 'Erik Hofman',
title: 'Re: [Flightgear-devel] Auto altimeter setting at startup (?)'
},
{
url: 'https://sourceforge.net/p/flightgear/mailman/message/35059961/',
author: 'Ludovic Brenta',
title: 'Re: [Flightgear-devel] dual-control-tools and the limit on packet size'
},
{
url: 'https://sourceforge.net/p/flightgear/mailman/message/20014126/',
author: 'Tim Moore',
title: 'Re: [Flightgear-devel] Cockpit displays (rendering, modelling)'
},
{
url: 'https://sourceforge.net/p/flightgear/mailman/message/23518343/',
author: 'Tim Moore',
title: '[Flightgear-devel] Atmosphere patch from John Denker'
} // add other tests below
], // end of vector with self-tests
// regex/xpath and transformations for extracting various required fields
author: {
xpath: 'tbody/tr[1]/td/div/small/text()',
transform: extract(/From: (.*) <.*@.*>/)
},
title: {
xpath: 'tbody/tr[1]/td/div/div[1]/b/a/text()'
},
date: {
xpath: 'tbody/tr[1]/td/div/small/text()',
transform: extract(/- (.*-.*-.*) /)
},
url: {
xpath: 'tbody/tr[1]/td/div/div[1]/b/a/@href',
transform: prepend('https://sourceforge.net')
}
}, // end of mailing list profile
// next website/URL (forum)
'FlightGear forum': {
enabled: true,
type: 'archive',
event: 'document.onmouseup', // when to invoke the event handler
event_handler: null, // the event handler to be invoked
url_reg: /https:\/\/forum\.flightgear\.org\/.*/,
content: {
selection: getSelectedHtml,
idStyle: /p[0-9]{6}/,
parentTag: [
'className',
'content',
'postbody'
],
transform: [
removeComments,
forum_quote2cquote,
forum_smilies2text,
forum_fontstyle2wikistyle,
forum_code2syntaxhighlight,
img2link,
a2wikilink,
vid2wiki,
list2wiki,
forum_br2newline
]
},
// vector with tests to be executed for sanity checks (unit testing)
// postings will be downloaded using the URL specified, and then the author/title
// fields extracted using the outer regex and matched against what is expected
tests: [
{
url: 'https://forum.flightgear.org/viewtopic.php?f=18&p=284108#p284108',
author: 'mickybadia',
title: 'OSM still PNG maps'
},
{
url: 'https://forum.flightgear.org/viewtopic.php?f=19&p=284120#p284120',
author: 'Thorsten',
title: 'Re: FlightGear\'s Screenshot Of The Month MAY 2016'
},
{
url: 'https://forum.flightgear.org/viewtopic.php?f=71&t=29279&p=283455#p283446',
author: 'Hooray',
title: 'Re: Best way to learn Canvas?'
},
{
url: 'https://forum.flightgear.org/viewtopic.php?f=4&t=1460&p=283994#p283994',
author: 'bugman',
title: 'Re: eurofighter typhoon'
} // add other tests below
], // end of vector with self-tests
author: {
xpath: 'div/div[1]/p/strong/a/text()'
},
title: {
xpath: 'div/div[1]/h3/a/text()'
},
date: {
xpath: 'div/div[1]/p/text()[2]',
transform: extract(/» (.*?[0-9]{4})/)
},
url: {
xpath: 'div/div[1]/p/a/@href',
transform: [
extract(/\.(.*)/),
prepend('https://forum.flightgear.org')
]
}
}
};
// hash to map URLs (wiki article, issue tracker, sourceforge link, forum thread etc) to existing wiki templates
var URL2TemplateTable = {
// placeholder for now
}; // TemplateTable
var EventHandlers = {
updateTarget: function () {
alert('not yet implement');
},
updateFormat: function () {
alert('not yet implement');
}
}; // EventHandlers
// output methods (alert and jQuery for now)
var OUTPUT = {
// Shows a window.prompt() message box
msgbox: function (msg) {
window.prompt('Copy to clipboard ' + Host.getScriptVersion(), msg);
Host.setClipboard(msg);
},
// Show a jQuery dialog
jQueryDiag: function (msg) {
// WIP: add separate Target/Format combo boxes for changing the template to be used (e.g. for refs instead of quotes)
var target_format = '<form name=\'target\'>Target: <select name=\'selection\' onchange=\'EventHandlers.updateTarget();\'><option value=\'0\'>to wiki</option><option value=\'1\'>to forum</option></select>Format: <select name=\'format\' onchange=\'EventHandlers.updateFormat();\'><option value=\'0\'>refonly</option><option value=\'1\'>fgcquote</option></select></form>';
//var style='background-image: url(' + GM_getResourceURL ('myLogo')+ '); background-attachment: local; background-position: center; background-repeat: no-repeat; background-size: 70%; opacity: 1.0;'
var diagDiv = $('<div id="MyDialog"><textarea id="quotedtext" rows="10"cols="80" style=" width: 320px; height: 320px">' + msg + '</textarea>' + target_format + '</div>');
var diagParam = {
title: 'Copy your quote with Ctrl+c ' + Host.getScriptVersion(),
modal: true,
width: 'auto',
buttons: [
/*
{
text: 'Check',
click: selfCheckDialog
},
*/
/*
{
text: 'Setup',
click: setupDialog
},
*/
{
text: 'Select all',
click: function () {
Host.setClipboard(msg);
$('#quotedtext').select();
}
},
{
text: 'OK',
click: function () {
Host.setClipboard(msg);
$(this).dialog('close');
}
}
]
};
diagDiv.dialog(diagParam);
}
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
var speechTransformations = [
// ordering is crucial here (most specific first, least specific/most generic last)
{query:/I have done/gi, replacement:'$author has done'},
{query:/I\'ve done/gi, replacement:'$author has done'}, //FIXME. queries should really be vectors ...
{query:/I have got/gi, replacement:'$author has got'},
{query:/I\'ve got/gi, replacement:'$author has got'},
{query:/I myself/gi, replacement:'$author himself'},
{query:/I am/gi, replacement:' $author is'},
{query:/I can/gi, replacement:'$author can'},
{query:/I have/gi, replacement:'$author has'},
{query:/I should/g, replacement:'$author should'},
{query:/I shall/gi, replacement:'$author shall'},
{query:/I may/gi, replacement:'$author may'},
{query:/I will/gi, replacement:'$author will'},
{query:/I would/gi, replacement:'$author would'},
{query:/by myself/gi, replacement:'by $author'},
{query:/and I/gi, replacement:'and $author'},
{query:/and me/gi, replacement:'and $author'},
{query:/and myself/gi, replacement:'and $author'},
// least specific stuff last (broad/generic stuff is kept as is, with author clarification added in parentheses)
{query:/ I /, replacement:'I ($author)'},
{query:/ me /, replacement:'me ($author)'},
{query:/ my /, replacement:'my ($author)'},
{query:/myself/, replacement:'myself ($author)'},
{query:/mine/, replacement:'$author'}
];
// try to assist in transforming speech using the transformation vector passed in
// still needs to be exposed via the UI
function transformSpeech(text, author, gender, transformations) {
// WIP: foreach transformation in vector, replace the search pattern with the matched string (replacing author/gender as applicable)
for(var i=0;i< transformations.length; i++) {
var token = transformations[i];
// patch the replacement string using the correct author name
var replacement = token.replacement.replace(/\$author/gi, author);
text = text.replace(token.query, replacement);
} // end of token transformation
// console.log("transformed text is:"+text);
return text;
} // transformSpeech
// run a self-test
(function() {
var author ="John Doe";
var transformed = transformSpeech("I have decided to commit a new feature", author, null, speechTransformations );
if (transformed !== author+" has decided to commit a new feature")
Host.dbLog("FIXME: Speech transformations are not working correctly");
}) ();
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
var MONTHS = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
];
// Conversion for forum emoticons
var EMOTICONS = [
[/:shock:/g,
'O_O'],
[
/:lol:/g,
'(lol)'
],
[
/:oops:/g,
':$'
],
[
/:cry:/g,
';('
],
[
/:evil:/g,
'>:)'
],
[
/:twisted:/g,
'3:)'
],
[
/:roll:/g,
'(eye roll)'
],
[
/:wink:/g,
';)'
],
[
/:!:/g,
'(!)'
],
[
/:\?:/g,
'(?)'
],
[
/:idea:/g,
'(idea)'
],
[
/:arrow:/g,
'(->)'
],
[
/:mrgreen:/g,
'xD'
]
];
// ##################
// # Main functions #
// ##################
window.addEventListener('load', init);
Host.dbLog('matched page, load handler registered');
// Initialize (matching page loaded)
function init() {
Host.dbLog('page load handler invoked');
var profile = getProfile();
Host.dbLog("Profile type is:"+profile.type);
// Dispatch to correct event handler (depending on website/URL)
// TODO: this stuff could/should be moved into the config hash itself
if (profile.type=='wiki') {
profile.event_handler(); // just for testing
return;
}
Host.dbLog('using default mode');
//document.onmouseup = instantCquote;
// preparations for moving the the event/handler logic also into the profile hash, so that the wiki (edit mode) can be handled equally
eval(profile.event+"=instantCquote");
} // init()
// The main function
function instantCquote() {
var profile = getProfile();
// TODO: use config hash here
var selection = document.getSelection(),
output = {
},
field = {
},
post_id=0;
try {
post_id = getPostId(selection, profile);
}
catch (error) {
Host.dbLog('Failed extracting post id\nProfile:' + profile);
return;
}
if (selection.toString() === '') {
Host.dbLog('No text is selected, aborting function');
return;
}
if (!checkValid(selection, profile)) {
Host.dbLog('Selection is not valid, aborting function');
return;
} // TODO: this needs to be refactored so that it can be also reused by the async/AJAX mode
// to extract fields in the background (i.e. move to a separate function)
Host.dbLog("Starting extraction/transformation loop");
for (field in profile) {
if (field === 'name') continue;
if (field ==='type' || field === 'event' || field === 'event_handler') continue; // skip fields that don't contain xpath expressions
Host.dbLog("Extracting field using field id:"+post_id);
var fieldData = extractFieldInfo(profile, post_id, field);
var transform = profile[field].transform;
if (transform !== undefined) {
Host.dbLog('Field \'' + field + '\' before transformation:\n\'' + fieldData + '\'');
fieldData = applyTransformations(fieldData, transform);
Host.dbLog('Field \'' + field + '\' after transformation:\n\'' + fieldData + '\'');
}
output[field] = fieldData;
} // extract and transform all fields for the current profile (website)
Host.dbLog("extraction and transformation loop finished");
output.content = stripWhitespace(output.content);
output = createCquote(output);
outputText(output);
}
function getPostingDataAJAX(profile, url) {
Host.dbLog("Fetching posting via AJAX helper: "+url);
Host.dbLog("Source profile is="+profile);
var expr=CONFIG[profile].title.xpath;
Host.dbLog("xpath expression is:"+expr);
Host.download(url, function (response) {
Host.dbLog("Download status:"+response.statusText);
var blob = response.responseText;
var doc = Host.make_doc(blob,'text/html'); //new DOMParser().parseFromString(blob,'text/html');
// var xpath = '//*[@id="' + id + '"]/' + profile[field].xpath;
var xpath = '//'+expr; // this is simplified, because the real regex is more complex (see above), but we don't have the post ID when downloading postings via AJAX
var result = Host.eval_xpath(doc,xpath).stringValue; // doc.evaluate(xpath, doc, null, XPathResult.STRING_TYPE, null);
Host.dbLog("title:"+result);
}); // AJAX callback
} // getPostingDataAJAX
function runProfileTests() {
for (var profile in CONFIG) {
if (CONFIG[profile].type != 'archive') continue; // skip the wiki entry, because it's not an actual archive that we need to test
for (var test in CONFIG[profile].tests) {
var required_data = CONFIG[profile].tests[test];
var title = required_data.title;
//dbLog('Running test for posting titled:' + title);
// fetch posting via getPostingDataAJAX() and compare to the fields we are looking for (author, title, date)
getPostingDataAJAX(profile, required_data.url);
} // foreach test
} // foreach profile (website)
} //runProfileTests
function selfCheckDialog() {
var sections = '<h3>Important APIs:</h3><div id="api_checks"><font color="red">(to be added here using the Environment.runAPITests() helper)</font></div>';
try {
runProfileTests.call(undefined); // check website profiles
}
catch (e) {
alert(e.message);
}
for (var profile in CONFIG) {
// TODO: also check if enabled or not
if (CONFIG[profile].type != 'archive') continue; // skip the wiki entry, because it's not an actual archive that we need to test
var test_results = '';
for (var test in CONFIG[profile].tests) {
// var fieldData = extractFieldInfo(profile, post_id, 'author');
test_results += CONFIG[profile].tests[test].title + '<p/>';
}
sections +='<h3>' + profile + ':<font color="blue">'+ CONFIG[profile].url_reg+'</font></h3><div><p>' + test_results + '</p></div>\n';
} // https://jqueryui.com/accordion/
var checkDlg = $('<div id="selfCheck" title="Self Check dialog"><p><div id="accordion">' + sections + '</div></p></div>');
// run all API tests, invoke the callback to obtain the status
Environment.runAPITests(Host, function(meta) {
//console.log('Running API test '+meta.name);
meta.test(function(result) {
var status = (result)?'success':'fail';
var test = $("<p></p>").text('Running API test '+meta.name+':'+status); ;
$('#api_checks', checkDlg).append(test);
});
});
//$('#accordion',checkDlg).accordion();
checkDlg.dialog({
width: 700,
height: 500,
open: function () {
// http://stackoverflow.com/questions/2929487/putting-a-jquery-ui-accordion-in-a-jquery-ui-dialog
$('#accordion').accordion({
autoHeight: true
});
}
}); // show dialog
} // selfCheckDialog
// show a simple configuration dialog (WIP)
function setupDialog() {
//alert("configuration dialog is not yet implemented");
var checked = (Host.get_persistent('debug_mode_enabled', false) === true) ? 'checked' : '';
//dbLog("value is:"+get_persistent("debug_mode_enabled"));
//dbLog("persistent debug flag is:"+checked);
var setupDiv = $('<div id="setupDialog" title="Setup dialog">NOTE: this configuration dialog is still work-in-progress</p><label><input id="debugcb" type="checkbox"' + checked + '>Enable Debug mode</label><p/><div id="progressbar"></div></div>');
setupDiv.click(function () {
//alert("changing persistent debug state");
Host.set_persistent('debug_mode_enabled', $('#debugcb').is(':checked'));
});
//MediaWiki editing stub, based on: https://www.mediawiki.org/wiki/API:Edit#Editing_via_Ajax
//only added here to show some status info in the setup dialog
Host.download('http://wiki.flightgear.org/api.php?action=query&prop=info|revisions&intoken=edit&rvprop=timestamp&titles=Main%20Page', function (response) {
var message = 'FlightGear wiki login status (AJAX):';
var status = response.statusText;
var color = (status == 'OK') ? 'green' : 'red';
Host.dbLog(message + status);
var statusDiv = $('<p>' + message + status + '</p>').css('color', color);
setupDiv.append(statusDiv);
});
setupDiv.dialog();
} // setupDialog
// this can be used to download/cache $FG_ROOT/options.xml so that fgfs CLI arguments can be recognized automatically
// which can help transforming postings correctly
function downloadOptionsXML() {
// download $FG_ROOT/options.xml
Host.download("https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/options.xml?format=raw", function(response) {
var xml = response.responseText;
var doc = Host.make_doc(xml, 'text/xml');
// https://developer.mozilla.org/en-US/docs/Web/API/XPathResult
var options = Host.eval_xpath(doc, '//*/option', XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
// http://help.dottoro.com/ljgnejkp.php
Host.dbLog("Number of options found in options.xml:"+options.snapshotLength);
// http://help.dottoro.com/ljtfvvpx.php
// https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/options.xml
}); // end of options.xml download
} // downloadOptionsXML
function getProfile(url=undefined) {
if(url === undefined)
url=window.location.href;
else
url=url;
Host.dbLog("getProfile call URL is:"+url);
for (var profile in CONFIG) {
if (url.match(CONFIG[profile].url_reg) !== null) {
Host.dbLog('Matching website profile found');
var invocations = Host.get_persistent(Host.getScriptVersion(), 0);
Host.dbLog('Number of script invocations for version ' + Host.getScriptVersion() + ' is:' + invocations);
// determine if we want to show a config dialog
if (invocations === 0) {
Host.dbLog("ask for config dialog to be shown");
var response = confirm('This is your first time running version ' + Host.getScriptVersion() + '\nConfigure now?');
if (response) {
// show configuration dialog (jQuery)
setupDialog();
}
else {
} // don't configure
} // increment number of invocations, use the script's version number as the key, to prevent the config dialog from showing up again (except for updated scripts)
Host.dbLog("increment number of script invocations");
Host.set_persistent(Host.getScriptVersion(), invocations + 1);
return CONFIG[profile];
}
Host.dbLog('Could not find matching URL in getProfile() call!');
}
}// Get the HTML code that is selected
function getSelectedHtml() {
// From http://stackoverflow.com/a/6668159
var html = '',
selection = document.getSelection();
if (selection.rangeCount) {
var container = document.createElement('div');
for (var i = 0; i < selection.rangeCount; i++) {
container.appendChild(selection.getRangeAt(i).cloneContents());
}
html = container.innerHTML;
}
Host.dbLog('instantCquote(): Unprocessed HTML\n\'' + html + '\'');
return html;
}// Gets the selected text
function getSelectedText() {
return document.getSelection().toString();
}// Get the ID of the post
// (this needs some work so that it can be used by the AJAX mode, without an actual selection)
function getPostId(selection, profile, focus) {
if (focus !== undefined) {
Host.dbLog("Trying to get PostId with defined focus");
selection = selection.focusNode.parentNode;
} else {
Host.dbLog("Trying to get PostId with undefined focus");
selection = selection.anchorNode.parentNode;
}
while (selection.id.match(profile.content.idStyle) === null) {
selection = selection.parentNode;
}
Host.dbLog("Selection id is:"+selection.id);
return selection.id;
}
// Checks that the selection is valid
function checkValid(selection, profile) {
var ret = true,
selection_cp = {
},
tags = profile.content.parentTag;
for (var n = 0; n < 2; n++) {
if (n === 0) {
selection_cp = selection.anchorNode.parentNode;
} else {
selection_cp = selection.focusNode.parentNode;
}
while (true) {
if (selection_cp.tagName === 'BODY') {
ret = false;
break;
} else {
var cont = false;
for (var i = 0; i < tags.length; i++) {
if (selection_cp[tags[0]] === tags[i]) {
cont = true;
break;
}
}
if (cont) {
break;
} else {
selection_cp = selection_cp.parentNode;
}
}
}
}
ret = ret && (getPostId(selection, profile) === getPostId(selection, profile, 1));
return ret;
}// Extracts the raw text from a certain place, using an XPath
function extractFieldInfo(profile, id, field) {
if (field === 'content') {
Host.dbLog("Returning content (selection)");
return profile[field].selection();
} else {
Host.dbLog("Extracting field via xpath:"+field);
var xpath = '//*[@id="' + id + '"]/' + profile[field].xpath;
return Host.eval_xpath(document, xpath).stringValue; // document.evaluate(xpath, document, null, XPathResult.STRING_TYPE, null).stringValue;
}
}// Change the text using specified transformations
function applyTransformations(fieldInfo, trans) {
if (typeof trans === 'function') {
return trans(fieldInfo);
} else if (Array.isArray(trans)) {
for (var i = 0; i < trans.length; i++) {
fieldInfo = trans[i](fieldInfo);
Host.dbLog('applyTransformations(): Multiple transformation, transformation after loop #' + (i + 1) + ':\n\'' + fieldInfo + '\'');
}
return fieldInfo;
}
}// Formats the quote
function createCquote(data, light_quotes = true) {
// skip FGCquote (experimental)
if (light_quotes) return nonQuotedRef(data);
var date_added = new Date();
var wikiText = '{{FGCquote\n' + ((data.content.match(/^\s*?{{cquote/) === null) ? '|1= ' : '| ') + data.content + '\n' +
'|2= ' + createCiteWeb(data) + '\n' +
'}}';
return wikiText;
}
function nonQuotedRef(data) {
return addContentBlob(data) + createRefCite(data);
}//
function addContentBlob(data) {
return data.content;
}// wrap citation in ref tags
function createRefCite(data) {
return '<ref>' + createCiteWeb(data) + '</ref>';
}
function createCiteWeb(data) {
var date_added = new Date();
var wikiText = '{{cite web\n' +
' |url = ' + data.url + '\n' +
' |title = ' + nowiki(data.title) + '\n' +
' |author = ' + nowiki(data.author) + '\n' +
' |date = ' + datef(data.date) + '\n' +
' |added = ' + datef(date_added.toDateString()) + '\n' +
' |script_version = ' + GM_info.script.version + '\n' +
' }}\n';
return wikiText;
}// Output the text.
// Tries the jQuery dialog, and falls back to window.prompt()
function outputText(msg) {
try {
OUTPUT.jQueryDiag(msg);
// TODO: unify code & call Host.setClipboard() here
}
catch (err) {
msg = msg.replace(/<\/syntaxhighligh(.)>/g, '</syntaxhighligh$1');
OUTPUT.msgbox(msg);
}
}// #############
// # Utilities #
// #############
function extract(regex) {
return function (text) {
return text.match(regex) [1];
};
}
function prepend(prefix) {
return function (text) {
return prefix + text;
};
}
function removeComments(html) {
return html.replace(/<!--.*?-->/g, '');
}// Not currently used (as of June 2015), but kept just in case
// currently unused
function escapePipes(html) {
html = html.replace(/\|\|/g, '{{!!}}');
html = html.replace(/\|\-/g, '{{!-}}');
return html.replace(/\|/g, '{{!}}');
}// Converts HTML <a href="...">...</a> tags to wiki links, internal if possible.
function a2wikilink(html) {
// Links to wiki images, because
// they need special treatment, or else they get displayed.
html = html.replace(/<a.*?href="http:\/\/wiki\.flightgear\.org\/File:(.*?)".*?>(.*?)<\/a>/g, '[[Media:$1|$2]]');
// Wiki links without custom text.
html = html.replace(/<a.*?href="http:\/\/wiki\.flightgear\.org\/(.*?)".*?>http:\/\/wiki\.flightgear\.org\/.*?<\/a>/g, '[[$1]]');
// Links to the wiki with custom text
html = html.replace(/<a.*?href="http:\/\/wiki\.flightgear\.org\/(.*?)".*?>(.*?)<\/a>/g, '[[$1|$2]]');
// Remove underscores from all wiki links
var list = html.match(/\[\[.*?\]\]/g);
if (list !== null) {
for (var i = 0; i < list.length; i++) {
html = html.replace(list[i], underscore2Space(list[i]));
}
} // Convert non-wiki links
// TODO: identify forum/devel list links, and use the AJAX/Host.download helper to get a title/subject for unnamed links (using the existing xpath/regex helpers for that)
html = html.replace(/<a.*?href="(.*?)".*?>(.*?)<\/a>/g, '[$1 $2]');
// Remove triple dots from external links.
// Replace with raw URL (MediaWiki converts it to a link).
list = html.match(/\[.*?(\.\.\.).*?\]/g);
if (list !== null) {
for (var i = 0; i < list.length; i++) {
html = html.replace(list[i], list[i].match(/\[(.*?) .*?\]/) [1]);
}
}
return html;
}// Converts images, including images in <a> links
function img2link(html) {
html = html.replace(/<a[^<]*?href="([^<]*?)"[^<]*?><img.*?src="http:\/\/wiki\.flightgear\.org\/images\/.*?\/.*?\/(.*?)".*?><\/a>/g, '[[File:$2|250px|link=$1]]');
html = html.replace(/<img.*?src="http:\/\/wiki\.flightgear\.org\/images\/.*?\/.*?\/(.*?)".*?>/g, '[[File:$1|250px]]');
html = html.replace(/<a[^<]*?href="([^<]*?)"[^<]*?><img.*?src="(.*?)".*?><\/a>/g, '(see [$2 image], links to [$1 here])');
return html.replace(/<img.*?src="(.*?)".*?>/g, '(see the [$1 linked image])');
}// Converts smilies
function forum_smilies2text(html) {
html = html.replace(/<img src="\.\/images\/smilies\/icon_.*?\.gif" alt="(.*?)".*?>/g, '$1');
for (var i = 0; i < EMOTICONS.length; i++) {
html = html.replace(EMOTICONS[i][0], EMOTICONS[i][1]);
}
return html;
}// Converts font formatting
function forum_fontstyle2wikistyle(html) {
html = html.replace(/<span style="font-weight: bold">(.*?)<\/span>/g, '\'\'\'$1\'\'\'');
html = html.replace(/<span style="text-decoration: underline">(.*?)<\/span>/g, '<u>$1</u>');
html = html.replace(/<span style="font-style: italic">(.*?)<\/span>/g, '\'\'$1\'\'');
return html.replace(/<span class="posthilit">(.*?)<\/span>/g, '$1');
}// Converts code blocks
function forum_code2syntaxhighlight(html) {
var list = html.match(/<dl class="codebox">.*?<code>(.*?)<\/code>.*?<\/dl>/g),
data = [
];
if (list === null) return html;
for (var n = 0; n < list.length; n++) {
data = html.match(/<dl class="codebox">.*?<code>(.*?)<\/code>.*?<\/dl>/);
html = html.replace(data[0], processCode(data));
}
return html;
}// Strips any whitespace from the beginning and end of a string
function stripWhitespace(html) {
html = html.replace(/^\s*?(\S)/, '$1');
return html.replace(/(\S)\s*?\z/, '$1');
}// Process code, including basic detection of language
function processCode(data) {
var lang = '',
code = data[1];
code = code.replace(/ /g, ' ');
if (code.match(/=?.*?\(?.*?\)?;/) !== null) lang = 'nasal';
if (code.match(/<.*?>.*?<\/.*?>/) !== null || code.match(/<!--.*?-->/) !== null) lang = 'xml';
code = code.replace(/<br\/?>/g, '\n');
return '<syntaxhighlight lang="' + lang + '" enclose="div">\n' + code + '\n</syntaxhighlight>';
}// Converts quote blocks to Cquotes
function forum_quote2cquote(html) {
html = html.replace(/<blockquote class="uncited"><div>(.*?)<\/div><\/blockquote>/g, '{{cquote|$1}}');
if (html.match(/<blockquote>/g) === null) return html;
var numQuotes = html.match(/<blockquote>/g).length;
for (var n = 0; n < numQuotes; n++) {
html = html.replace(/<blockquote><div><cite>(.*?) wrote.*?:<\/cite>(.*?)<\/div><\/blockquote>/, '{{cquote|$2|$1}}');
}
return html;
}// Converts videos to wiki style
function vid2wiki(html) {
// YouTube
html = html.replace(/<div class="video-wrapper">\s.*?<div class="video-container">\s*?<iframe class="youtube-player".*?width="(.*?)" height="(.*?)" src="http:\/\/www\.youtube\.com\/embed\/(.*?)".*?><\/iframe>\s*?<\/div>\s*?<\/div>/g, '{{#ev:youtube|$3|$1x$2}}');
// Vimeo
html = html.replace(/<iframe src="http:\/\/player\.vimeo\.com\/video\/(.*?)\?.*?" width="(.*?)" height="(.*?)".*?>.*?<\/iframe>/g, '{{#ev:vimeo|$1|$2x$3}}');
return html.replace(/\[.*? Watch on Vimeo\]/g, '');
}// Not currently used (as of June 2015), but kept just in case
// currently unused
function escapeEquals(html) {
return html.replace(/=/g, '{{=}}');
}// <br> to newline.
function forum_br2newline(html) {
html = html.replace(/<br\/?><br\/?>/g, '\n');
return html.replace(/<br\/?>/g, '\n\n');
}// Forum list to wiki style
function list2wiki(html) {
var list = html.match(/<ul>(.*?)<\/ul>/g);
if (list !== null) {
for (var i = 0; i < list.length; i++) {
html = html.replace(/<li>(.*?)<\/li>/g, '* $1\n');
}
}
list = html.match(/<ol.*?>(.*?)<\/ol>/g);
if (list !== null) {
for (var i = 0; i < list.length; i++) {
html = html.replace(/<li>(.*?)<\/li>/g, '# $1\n');
}
}
html = html.replace(/<\/?[uo]l>/g, '');
return html;
}
function nowiki(text) {
return '<nowiki>' + text + '</nowiki>';
}// Returns the correct ordinal adjective
function ordAdj(date) {
date = date.toString();
if (date == '11' || date == '12' || date == '13') {
return 'th';
} else if (date.substr(1) == '1' || date == '1') {
return 'st';
} else if (date.substr(1) == '2' || date == '2') {
return 'nd';
} else if (date.substr(1) == '3' || date == '3') {
return 'rd';
} else {
return 'th';
}
}
// Formats the date to this format: Apr 26th, 2015
function datef(text) {
var date = new Date(text);
return MONTHS[date.getMonth()] + ' ' + date.getDate() + ordAdj(date.getDate()) + ', ' + date.getFullYear();
}
function underscore2Space(str) {
return str.replace(/_/g, ' ');
}