// ==UserScript==
// @name Instant-Cquotes
// @name:it Instant-Cquotes
// @license public domain
// @version 0.36
// @date 2016-05-05
// @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)
// @supportURL http://wiki.flightgear.org/FlightGear_wiki:Instant-Cquotes
// @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-start
// @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
* - support RSS feeds http://dir.gmane.org/gmane.games.flightgear.devel/
* - 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 (dont require ES6)
* - 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(xpi=false) {
if(xpi) {
Environment.scriptEngine = 'firefox addon';
console.log('in firefox xpi/addon mode');
return Environment.FirefoxAddon; // HACK for testing the xpi mode (firefox addon)
}
// 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 + '.');
//console.log("not in firefox addon mode...");
// 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
// NOTE: This mode/environment is WIP and highly experimental ...
// To see this working, you need to package up the whole file as a firefox xpi using "jpm xpi"
// and then start the whole thing via "jpm run", to do that, you also need a matching package.json (i.e. via jpm init)
FirefoxAddon: {
init: function() {
console.log("Firefox addon mode ...");
},
getScriptVersion: function() {
return '0.36'; // FIXME
},
dbLog: function(msg) {
console.log(msg);
},
addEventListener: function(ev, cb) {
require("sdk/tabs").on("ready", logURL);
function logURL(tab) {
console.log("URL loaded:" + tab.url);
}
},
registerConfigurationOption: function() {
// https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Add_a_Context_Menu_Item
console.log("context menu support n/a");
},
registerTrigger: function() {
// https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Add_a_Context_Menu_Item
// https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/context-menu#Item%28options%29
var contextMenu = require("sdk/context-menu");
var menuItem = contextMenu.Item({
label: "Instant Cquote",
context: contextMenu.SelectionContext(),
contentScript: 'self.on("click", function () {' +
' var text = window.getSelection().toString();' +
' self.postMessage(text);' +
'});',
onMessage: function (selectionText) {
console.log(selectionText);
}
});
}, //registerTrigger
get_persistent: function(key, default_value) {return default_value;},
set_persistent: function(key, value) {
console.log("persistence stubs not yet filled in !");
},
set_clipboard: function() {
// https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/selection
console.log('clipboard stub not yet filled in ...');
}
}, // end of FireFox addon config
///////////////////////////////////////
// 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.registerConfigurationOption(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()
registerConfigurationOption: 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()
registerTrigger: function() {
// TODO: we can use the following callback non-interactively, i.e. to trigger background tasks
// http://javascript.info/tutorial/onload-ondomcontentloaded
document.addEventListener("DOMContentLoaded", function(event) {
console.log("Instant Cquotes: DOM fully loaded and parsed");
});
window.addEventListener('load', init); // page fully loaded
Host.dbLog('Instant Cquotes: page load handler registered');
// Initialize (matching page loaded)
function init() {
console.log('Instant Cquotes: 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;
// HACK: 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()
}, // registerTrigger
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
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);
//UI.alert('Saved value for key\n'+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() {
// hard-coded default template
var template = '$CONTENT{{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 a saved template if found, fall back to hard-coded one above otherwise
return Host.get_persistent('default_template', 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
} // forum
}; // CONFIG has
// 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
// 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);
}, // msgbox
jQueryTabbed: function(msg, original) {
// 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)
For now, this is WIP - in the future, there will be a dropdown menu added and all templates will be editable.
This tab contains script specific settings
One day, this tab may contain help....
show some script related information here
`); // tabs div
// add dynamic elements to each tab
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() );
$('div#templates div#template_area', markup).append(templateArea);
//$('#templates', markup).append($('