// ==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 = $('