// ==UserScript==
// @name Instant-Cquotes
// @name:it Instant-Cquotes
// @version 0.35
// @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
// @downloadURL none
// ==/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.
//
/* Here are some TODOs
*
* - move event handling/processing to the CONFIG hash
* - use try/catch more widely
* - wrap function calls in try/call for better debugging/diagnostics
* - add helpers for [].forEach.call, map, apply and call
* - replace for/in, for/of, let statements for better compatibility
* - for the same reason, replace use of functions with default params
* - isolate UI (e.g. JQUERY) code in UserInterface hash
* - expose regex/transformations via the UI
*
*
*
*/
'use strict';
// TODO: move to GreaseMonkey/UI host
// prevent conflicts with jQuery used on webpages: https://wiki.greasespot.net/Third-Party_Libraries#jQuery
//this.$ = this.jQuery = jQuery.noConflict(true);
// this hash is just intended to help isolate UI specifics
// so that we don't need to maintain/port tons of code
var UserInterface = {
get: function() {
return UserInterface.DEFAULT;
},
CONSOLE: {
}, // CONSOLE (shell, mainly useful for testing)
DEFAULT: {
alert: function(msg) {return window.alert(msg); },
prompt: function(msg) {return window.prompt(msg); },
confirm: function(msg) {return window.confirm(msg); },
dialog: null,
selection: null
}, // default UI mapping (Browser/User script)
JQUERY: {
} // JQUERY
}; // UserInterface
var UI = UserInterface.get(); // DEFAULT for now
// 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)
UI.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.content.substring(result.content.indexOf(',')+1, result.content.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
{
name:"download $FG_ROOT/options.xml", test: function(recipient) {
downloadOptionsXML();
recipient(true);
} // 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
recipient.call(undefined, test);
} // 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 result = {}; // hash to be returned
[].forEach.call(['author','date','title','content'], function(field) {
var xpath_query = '//' + profile[field].xpath;
try {
var value = Host.eval_xpath(doc, xpath_query).stringValue;
//UI.alert("extracted field value:"+value);
// now apply all transformations, if any
var value = applyTransformations(value, profile[field].transform );
result[field]=value; // store the extracted/transormed value in the hash that we pass on
} // try
catch(e) {
UI.alert("downloadPosting failed:\n"+ e.message);
} // catch
}); // forEach field
EventHandler(result); // pass the result to the handler
}); // call to Host.download()
}, // 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()
getTemplate: function() {
var template = '{{cite web\n' +
' |url = $URL \n' +
' |title = $TITLE \n' +
' |author = $AUTHOR \n' +
' |date = $DATE \n' +
' |added = $ADDED \n' +
' |script_version = $SCRIPT_VERSION \n' +
' }}\n';
return template;
} // getTemplate
} // 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();
// hash with supported websites/URLs, includes xpath and regex expressions to extract certain fields, and a vector with optional transformations for post-processing each field
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)
//for each supported mode, invoke the trigger and call the corresponding handler
[].forEach.call(CONFIG['FlightGear.wiki'].modes, function(mode) {
//dbLog("Checking trigger:"+mode.name);
if(mode.trigger) {
mode.handler();
}
});
}, // the event handler to be invoked
url_reg: '^(http|https)://wiki.flightgear.org', // ignore for now: not currently used by the wiki mode
modes: [
{ name:'process-editSections',
trigger: function() {return true;}, // match URL regex - return true for always match
// the code implementing the mode
handler: function() {
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
} // handler
} // process-editSections
// TODO: add other wiki modes below
] // modes
}, // 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 (TODO:add content xpath to forum hash)
selection: getSelectedText,
idStyle: /msg[0-9]{8}/,
parentTag: [
'tagName',
'PRE'
],
transform: [],
}, // 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',
date: 'May 3rd, 2016', // NOTE: using the transformed date here
title: 'Re: [Flightgear-devel] Auto altimeter setting at startup (?)'
},
{
url: 'https://sourceforge.net/p/flightgear/mailman/message/35059961/',
author: 'Ludovic Brenta',
date: 'May 3rd, 2016',
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',
date: 'Aug 4th, 2008',
title: 'Re: [Flightgear-devel] Cockpit displays (rendering, modelling)'
},
{
url: 'https://sourceforge.net/p/flightgear/mailman/message/23518343/',
author: 'Tim Moore',
date: 'Sep 10th, 2009',
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()',
transform:[]
},
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: {
xpath: '', //TODO: this must be added for downloadPosting() to work
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
// NOTE: forum postings can be edited, so that these tests would fail - thus, it makes sense to pick locked topics/postings for such tests
tests: [
{
url: 'https://forum.flightgear.org/viewtopic.php?f=18&p=284108#p284108',
author: 'mickybadia',
date: '',
title: 'OSM still PNG maps'
},
{
url: 'https://forum.flightgear.org/viewtopic.php?f=19&p=284120#p284120',
author: 'Thorsten',
date: '',
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',
date: '',
title: 'Re: Best way to learn Canvas?'
},
{
url: 'https://forum.flightgear.org/viewtopic.php?f=4&t=1460&p=283994#p283994',
author: 'bugman',
date: '',
title: 'Re: eurofighter typhoon'
} // add other tests below
], // end of vector with self-tests
author: {
xpath: 'div/div[1]/p/strong/a/text()',
transform: []
},
title: {
xpath: 'div/div[1]/h3/a/text()',
transform: []
},
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')
] // transform vector
} // url
}
};
// hash to map URLs (wiki article, issue tracker, sourceforge link, forum thread etc) to existing wiki templates
var MatchURL2Templates = [
// placeholder for now
{
name: 'rewrite sourceforge code links',
url_reg: '',
handler: function() {
} // handler
} // add other templates below
]; // MatchURL2Templates
var EventHandlers = {
updateTarget: function () {
UI.alert('not yet implement');
},
updateFormat: function () {
UI.alert('not yet implement');
}
}; // EventHandlers
// output methods (alert and jQuery for now)
var OUTPUT = {
// Shows a window.prompt() message box
msgbox: function (msg) {
UI.prompt('Copy to clipboard ' + Host.getScriptVersion(), msg);
Host.setClipboard(msg);
},
jQueryTabbed: function(msg) {
// FIXME: using backtics here makes the whole thing require ES6 ....
var markup = $(`
This tab contains your extracted and post-processed selection
This tab contains articles that you can directly access/edit using the mediawiki API
This tab contains templates for different types of articles (newsletter, changelog, release plan etc)
This tab contains script specific settings
One day, this tab may contain help....
show some script related information here
`);
var help = $('#helpButton', markup);
help.button();
help.click(function() {
window.open("http://wiki.flightgear.org/FlightGear_wiki:Instant-Cquotes");
});
// rows="10"cols="80" style=" width: 420px; height: 350px"
var textarea = $('');
textarea.val(msg);
$('#selection #content', markup).append(textarea);
var templateArea = $('');
templateArea.val( Host.getTemplate() );
$('#templates', markup).append(templateArea);
// TODO: Currently, this is hard-coded, but should be made customizable via the "articles" tab at some point ...
var articles = [
{name: 'FAQ', url:'http://wiki.flightgear.org/Frequently_asked_questions'},
{name:'Next Newsletter', url:'http://wiki.flightgear.org/index.php?title=Next_newsletter'},
{name:'Next Changelog', url:'http://wiki.flightgear.org/index.php?title=Next_changelog'},
{name:'Lessons learnt', url:'http://wiki.flightgear.org/Release_plan/Lessons_learned'} // TODO: use wikimedia template
];
// TODO: this should be moved elsewhere
function updateArticleList(selector) {
$.each(articles, function (i, article) {
$(selector, markup).append($('