// ==UserScript== // @name Instant-Cquotes // @name:it Instant-Cquotes // @license public domain // @version 0.39 // @date 2016-05-20 // @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 // @require https://cdn.jsdelivr.net/genetic.js/0.1.14/genetic.js // @require https://cdn.jsdelivr.net/synaptic/1.0.4/synaptic.min.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. // // This script has a number of dependencies that are implicitly satisfied when run as a user script // via GreaseMonkey/TamperMonkey; however, these need to be explicitly handled when using a different mode (e.g. firefox/android): // // - jQuery - user interface (REQUIRED) // - genetic-js - genetic programming (OPTIONAL/EXPERIMENTAL) // - synaptic - neural networks (OPTIONAL/EXPERIMENTAL) // // /* 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 // http://stackoverflow.com/a/5014220 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, populateWatchlist: function() { }, populateEditSections: function() { } }, // 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 (host.get_persistent('startup.disable_validation',false)) return; if(Environment.scriptEngine !== "Greasemonkey") console.log("NOTE: This script has not been tested with script engines other than GreaseMonkey recently!"); var dependencies = [ {name:'jQuery', test: function() {} }, {name:'genetic.js', test: function() {} }, {name:'synaptic', test: function() {} }, ]; [].forEach.call(dependencies, function(dep) { console.log("Checking for dependency:"+dep.name); var status=false; try { dep.test.call(undefined); status=true; } catch(e) { status=false; } finally { var success = (status)?'==> success':'==> failed'; console.log(success); return status; } }); }, // validate // 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) // ALSO: you will have to explicitly install any dependencies using jpm 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(name, callback, hook) { // https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Add_a_Context_Menu_Item console.log("config menu support n/a in firefox mode"); // https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Using_third-party_modules_%28jpm%29 var menuitems = require("menuitem"); var menuitem = menuitems.Menuitem({ id: "clickme", menuid: "menu_ToolsPopup", label: name, onCommand: function() { console.log("menuitem clicked:"); callback(); }, insertbefore: "menu_pageInfo" }); }, 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(), // https://developer.mozilla.org/en/Add-ons/SDK/Guides/Two_Types_of_Scripts // https://developer.mozilla.org/en-US/Add-ons/SDK/Guides/Content_Scripts contentScript: 'self.on("click", function () {' + ' var text = window.getSelection().toString();' + ' self.postMessage(text);' + '});', onMessage: function (selectionText) { console.log(selectionText); instantCquote(selectionText); } }); // for selection handling stuff, see: https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/selection function myListener() { console.log("A selection has been made."); } var selection = require("sdk/selection"); selection.on('select', myListener); }, //registerTrigger get_persistent: function(key, default_value) { // https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/simple-storage var ss = require("sdk/simple-storage"); console.log("firefox mode does not yet have persistence support"); return default_value;}, set_persistent: function(key, value) { console.log("firefox persistence stubs not yet filled in !"); }, set_clipboard: function(content) { // https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/clipboard //console.log('clipboard stub not yet filled in ...'); var clipboard = require("sdk/clipboard"); clipboard.set(content); } //set_cliipboard }, // end of FireFox addon config // placeholder for now ... Android: { // NOP }, // Android /////////////////////////////////////// // 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() // TODO: add makeAJAXCall, and makeWikiCall here // 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 subsequently used for API lookups // the Host hash contains all platform/browser-specific APIs var Host = Environment.getHost(); Environment.validate(Host); // this checks the obtained host to see if all required dependencies are available 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 // TODO: move downloadWatchlist() etc here 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 (not used atm) event_handler: null, // the event handler to be invoked (not used atm) url_reg: /https:\/\/forum\.flightgear\.org\/.*/, content: { xpath: '', //TODO: this must be added for downloadPosting() to work, or it cannot extract contents 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: 'May 3rd, 2016', title: 'OSM still PNG maps' }, { url: 'https://forum.flightgear.org/viewtopic.php?f=19&p=284120#p284120', author: 'Thorsten', date: 'May 3rd, 2016', 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: 'Apr 25th, 2016', title: 'Re: Best way to learn Canvas?' }, { url: 'https://forum.flightgear.org/viewtopic.php?f=4&t=1460&p=283994#p283994', author: 'bugman', date: 'May 2nd, 2016', title: 'Re: eurofighter typhoon' } // add other tests below ], // end of vector with self-tests author: { xpath: 'div/div[1]/p/strong/a/text()', transform: [] // no transformations applied }, title: { xpath: 'div/div[1]/h3/a/text()', transform: [] // no transformations applied }, 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 // this is currently work-in-progress, and will need to be refactored sooner or later // for now, functionality matters more than elegant design/code :) 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, converted to proper wikimedia markup, including proper attribution.
Note this is work-in-progress, i.e. not yet fully functional

This tab contains articles that you can directly access/edit using the mediawiki API
Note: The watchlist is retrieved dynamically, so does not need to be edited here


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 is a placeholder for features currently under development

Generation Fitness Expression Result
This tab will contain script specific settings
One day, this tab may contain help....

show some script related information here
`); // tabs div var evolve_regex = $('div#development button#evolve_regex', markup); evolve_regex.click(function() { //alert("Evolve regex"); evolve_expression_test(); }); var test_perceptron = $('div#development button#test_perceptron', markup); test_perceptron.click(function() { alert("Test perceptron"); }); // add dynamic elements to each tab // NOTE: this affects all template selectors, on all tabs $('select#template_select', markup).change(function() { UI.alert("Sorry, templates are not yet fully implemented (WIP)"); }); 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 = $('