Instant-Cquotes

Automatically converts selected FlightGear mailing list and forum quotes into post-processed MediaWiki markup (i.e. cquotes).

Old: v0.33 - 2016-05-06 - Change icon; begin populating the wiki mode
New: v0.34 - 2016-05-06 - Add sanity checks; introduce internal download helper

  • --- /tmp/diffy20240425-660288-2as2d3 2024-04-25 08:30:30.072876133 +0000
  • +++ /tmp/diffy20240425-660288-krq2le 2024-04-25 08:30:30.072876133 +0000
  • @@ -1,6 +1,7 @@
  • // ==UserScript==
  • // @name Instant-Cquotes
  • -// @version 0.33
  • +// @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)
  • @@ -14,11 +15,13 @@
  • // @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
  • @@ -49,7 +52,7 @@
  • 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") {
  • + 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.
  • }
  • @@ -63,7 +66,7 @@
  • },
  • validate: function(host) {
  • - if(Environment.scriptEngine !== "Greasemonkey" && host.get_persistent('startup.disable_validation',false)!=true)
  • + 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!");
  • },
  • @@ -72,28 +75,50 @@
  • // for a working example, refer to the JSON test at the end
  • // TODO: add jQuery tests
  • APITests: [
  • - {name:'download', test: function() {return true;} },
  • - {name:'make_doc', test: function() {return true;} },
  • - {name:'eval_xpath', test: function() {return true;} },
  • - {name:'JSON de/serialization', test: function() {
  • + {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);
  • - return JSON.stringify(hash1) === JSON.stringify(hash2);
  • + recipient(JSON.stringify(hash1) === JSON.stringify(hash2));
  • } // callback
  • },
  • - {name:'person speech transformation', test: function() {return true;} }
  • +
  • + // 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, callback) {
  • - for(var t in Environment.APITests ) {
  • - var test = Environment.APITests[t];
  • + 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);
  • + //callback(test);
  • + recipient.call(undefined, test);
  • + //test.call(undefined, callback)
  • +
  • } // foreach test
  • }, // runAPITests
  • @@ -117,10 +142,11 @@
  • {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() {
  • @@ -152,6 +178,34 @@
  • 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)
  • @@ -202,19 +256,14 @@
  • }; // 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)
  • -
  • -
  • -/*
  • -Environment.runAPITests(Host, function(meta) {
  • - console.log('Running API test '+meta.name);
  • -});
  • -*/
  • -
  • // 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);
  • @@ -248,12 +297,14 @@
  • var editSections = document.getElementsByClassName('mw-editsection');
  • console.log('FlightGear wiki article, number of edit sections: '+editSections.length);
  • -
  • - for (let editSection of editSections) {
  • - var note = document.createTextNode(' (instant-cquotes is lurking) ');
  • - editSection.appendChild(note);
  • - }
  • + // 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
  • @@ -266,6 +317,7 @@
  • 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: [
  • @@ -411,7 +463,9 @@
  • 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 diagDiv = $('<div id="MyDialog"><textarea id="quotedtext" rows="10"cols="80" style="width: 290px; height: 290px">' + msg + '</textarea>' + target_format + '</div>');
  • + //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,
  • @@ -451,43 +505,58 @@
  • //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  • -var speechTransform_1st_to_3rd = [
  • +var speechTransformations = [
  • // ordering is crucial here (most specific first, least specific/most generic last)
  • - {'I myself': '$author himself'},
  • - {'I am': ' $author is'},
  • - {'I can': '$author can'},
  • - {'I have': '$author has'},
  • - {'I should': '$author should'},
  • - {'I shall': '$author shall'},
  • - {'I may': '$author may'},
  • - {'I will': '$author will'},
  • - {'I would': '$author would'},
  • - {'by myself': 'by $author'},
  • - {'and I': 'and $author'},
  • - {'and me': 'and $author'},
  • - {'and myself': 'and $author'},
  • + {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)
  • - {'I': 'I ($author)'},
  • - {'me': 'me ($author)'},
  • - {'my': 'my ($author)'},
  • - {'myself': 'myself ($author)'},
  • - {'mine': '$author'}
  • + {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) {
  • - // TODO: foreach transformation in vector, replace the search pattern with the matched string (replacing author/gender as applicable)
  • + // 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, speechTransform_1st_to_3rd );
  • -if (transformed !== "John Doe has decided to commit a new feature")
  • +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");
  • }) ();
  • //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  • @@ -598,12 +667,14 @@
  • output = {
  • },
  • field = {
  • - };
  • -
  • + },
  • + post_id=0;
  • +
  • +
  • try {
  • - var post_id = getPostId(selection, profile);
  • + post_id = getPostId(selection, profile);
  • }
  • catch (error) {
  • - Host.dbLog('Failed extracting post id
  • -Profile:' + profile)
  • + Host.dbLog('Failed extracting post id
  • +Profile:' + profile);
  • return;
  • }
  • if (selection.toString() === '') {
  • @@ -678,14 +749,9 @@
  • } //runProfileTests
  • function selfCheckDialog() {
  • - var sections = '<h3>Important APIs:</h3><div id="api_checks">(to be added here using the Environment.runAPITests() helper)</div>';
  • + var sections = '<h3>Important APIs:</h3><div id="api_checks"><font color="red">(to be added here using the Environment.runAPITests() helper)</font></div>';
  • - Environment.runAPITests(Host, function(meta) {
  • - //sections.$('#api_checks').append(meta.name+'<p/>');
  • - console.log('Running API test '+meta.name);
  • - console.log((meta.test())?'success':'fail');
  • - });
  • try {
  • @@ -708,6 +774,21 @@
  • 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,
  • @@ -725,7 +806,7 @@
  • // 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' : '';
  • + 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>');
  • @@ -771,15 +852,23 @@
  • } // downloadOptionsXML
  • -function getProfile() {
  • +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 (window.location.href.match(CONFIG[profile].url_reg) !== null) {
  • + 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) {
  • + 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) {
  • @@ -793,10 +882,10 @@
  • } // 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)
  • + Host.set_persistent(Host.getScriptVersion(), invocations + 1);
  • return CONFIG[profile];
  • }
  • - Host.dbLog('Could not find matching URL in getProfile() call!')
  • + Host.dbLog('Could not find matching URL in getProfile() call!');
  • }
  • }// Get the HTML code that is selected
  • @@ -956,6 +1045,8 @@
  • 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, '{{!-}}');
  • @@ -1026,7 +1117,7 @@
  • }// Strips any whitespace from the beginning and end of a string
  • function stripWhitespace(html) {
  • - html = html.replace(/^\s*?(\S)/, '$1')
  • + html = html.replace(/^\s*?(\S)/, '$1');
  • return html.replace(/(\S)\s*?\z/, '$1');
  • }// Process code, including basic detection of language
  • @@ -1058,6 +1149,7 @@
  • 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.
  • @@ -1100,7 +1192,8 @@
  • } else {
  • return 'th';
  • }
  • -};
  • +}
  • +
  • // Formats the date to this format: Apr 26th, 2015
  • function datef(text) {
  • var date = new Date(text);