// ==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 = $('' + target_format + '
'); var diagParam = { title: 'Copy your quote with Ctrl+c ' + Host.getScriptVersion(), modal: true, width: 'auto', buttons: [ { text:'indirect speech', click: function() { // FIXME: this is quite a hack, but we were never planning to do this sort of thing // normally, this should be treated like a conventional transformation ... // anyway, for testing, use: https://sourceforge.net/p/flightgear/mailman/message/35069262/ // https://sourceforge.net/p/flightgear/mailman/message/35066974/ // which will look up {{cite web /* var posting = $('textarea#quotedtext',diagDiv).val(); var author = posting.substring(posting.indexOf('|author = '),posting.indexOf('')); var portion = posting.substring(0, posting.indexOf('{{cite web')+15); var indirectSpeech = transformSpeech(portion, author, null, speechTransformations ); */ //alert(indirectSpeech); //alert(posting); $('textarea#quotedtext',diagDiv).val('sorry transformSpeech() is not yet integrated ....'); } }, /* { text: 'Check', click: selfCheckDialog }, */ /* { text: 'Setup', click: setupDialog }, */ /* { text: 'Select all', click: function () { Host.setClipboard(msg); $('#quotedtext').select(); } }, */ { text: 'OK', click: function () { Host.setClipboard(msg); $(this).dialog('close'); } } ] }; diagDiv.dialog(diagParam); } }; ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // TODO: we can use an online API to help with some of this: http://www.eslnow.org/reported-speech-converter/ // See also: http://blog.mashape.com/list-of-25-natural-language-processing-apis/ // http://text-processing.com/docs/phrases.html // http://www.alchemyapi.com/ // https://words.bighugelabs.com/api.php // https://www.wordsapi.com/ // http://www.dictionaryapi.com/ // https://www.textrazor.com/ // http://www.programmableweb.com/news/how-5-natural-language-processing-apis-stack/analysis/2014/07/28 var speechTransformations = [ // TODO: support aliasing: would/should // ordering is crucial here (most specific first, least specific/most generic last) {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\'d suggest/gi, replacement:'$author would suggest'}, {query:/I myself/gi, replacement:'$author himself'}, {query:/I am/gi, replacement:' $author is'}, {query:/I can see/gi, replacement:'$author can see'}, {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) {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) { // 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, speechTransformations ); if (transformed !== author+" has decided to commit a new feature") Host.dbLog("FIXME: Speech transformations are not working correctly"); }) (); ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// var MONTHS = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]; // Conversion for forum emoticons var EMOTICONS = [ [/:shock:/g, 'O_O'], [ /:lol:/g, '(lol)' ], [ /:oops:/g, ':$' ], [ /:cry:/g, ';(' ], [ /:evil:/g, '>:)' ], [ /:twisted:/g, '3:)' ], [ /:roll:/g, '(eye roll)' ], [ /:wink:/g, ';)' ], [ /:!:/g, '(!)' ], [ /:\?:/g, '(?)' ], [ /:idea:/g, '(idea)' ], [ /:arrow:/g, '(->)' ], [ /:mrgreen:/g, 'xD' ] ]; // ################## // # Main functions # // ################## window.addEventListener('load', init); Host.dbLog('matched page, load handler registered'); // Initialize (matching page loaded) function init() { Host.dbLog('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; // 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() // The main function // TODO: split up, so that we can reuse the code elsewhere function instantCquote() { var profile = getProfile(); // TODO: use config hash here var selection = document.getSelection(), output = { }, field = { }, post_id=0; try { post_id = getPostId(selection, profile); } catch (error) { Host.dbLog('Failed extracting post id\nProfile:' + profile); return; } if (selection.toString() === '') { Host.dbLog('No text is selected, aborting function'); return; } if (!checkValid(selection, profile)) { Host.dbLog('Selection is not valid, aborting function'); return; } // TODO: this needs to be refactored so that it can be also reused by the async/AJAX mode // to extract fields in the background (i.e. move to a separate function) Host.dbLog("Starting extraction/transformation loop"); for (field in profile) { if (field === 'name') continue; if (field ==='type' || field === 'event' || field === 'event_handler') continue; // skip fields that don't contain xpath expressions Host.dbLog("Extracting field using field id:"+post_id); var fieldData = extractFieldInfo(profile, post_id, field); var transform = profile[field].transform; if (transform !== undefined) { Host.dbLog('Field \'' + field + '\' before transformation:\n\'' + fieldData + '\''); fieldData = applyTransformations(fieldData, transform); Host.dbLog('Field \'' + field + '\' after transformation:\n\'' + fieldData + '\''); } output[field] = fieldData; } // extract and transform all fields for the current profile (website) Host.dbLog("extraction and transformation loop finished"); output.content = stripWhitespace(output.content); output = createCquote(output); outputText(output); } /* function getPostingDataAJAX(profile, url) { Host.dbLog("Fetching posting via AJAX helper: "+url); Host.dbLog("Source profile is="+profile); var expr=CONFIG[profile].title.xpath; Host.dbLog("xpath expression is:"+expr); Host.download(url, function (response) { Host.dbLog("Download status:"+response.statusText); var blob = response.responseText; var doc = Host.make_doc(blob,'text/html'); //new DOMParser().parseFromString(blob,'text/html'); // var xpath = '//*[@id="' + id + '"]/' + profile[field].xpath; var xpath = '//'+expr; // this is simplified, because the real regex is more complex (see above), but we don't have the post ID when downloading postings via AJAX var result = Host.eval_xpath(doc,xpath).stringValue; // doc.evaluate(xpath, doc, null, XPathResult.STRING_TYPE, null); Host.dbLog("title:"+result); }); // AJAX callback } // getPostingDataAJAX */ function runProfileTests() { for (var profile in CONFIG) { if (CONFIG[profile].type != 'archive' || !CONFIG[profile].enabled ) continue; // skip the wiki entry, because it's not an actual archive that we need to test for (var test in CONFIG[profile].tests) { var required_data = CONFIG[profile].tests[test]; var title = required_data.title; //dbLog('Running test for posting titled:' + title); // fetch posting via getPostingDataAJAX() and compare to the fields we are looking for (author, title, date) //getPostingDataAJAX(profile, required_data.url); //alert("required title:"+title); } // foreach test } // foreach profile (website) } //runProfileTests function selfCheckDialog() { var sections = '

Important APIs:

'; try { runProfileTests.call(undefined); // check website profiles } catch (e) { UI.alert(e.message); } for (var profile in CONFIG) { // TODO: also check if enabled or not if (CONFIG[profile].type != 'archive') continue; // skip the wiki entry, because it's not an actual archive that we need to test var test_results = ''; for (var test in CONFIG[profile].tests) { // var fieldData = extractFieldInfo(profile, post_id, 'author'); test_results += CONFIG[profile].tests[test].title + '

'; } sections +='

' + profile + ':'+ CONFIG[profile].url_reg+'

' + test_results + '

\n'; } // https://jqueryui.com/accordion/ var checkDlg = $('

' + sections + '

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

").text('Running API test '+meta.name+':'+status); $('#api_checks', checkDlg).append(test); }); // update tests results }); // runAPITests /* [].forEach.call(CONFIG, function(profile) { alert("profile is:"+profile); [].forEach.call(CONFIG[profile].tests, function(test) { //UI.alert(test.url); Host.downloadPosting(test.url, function(downloaded) { alert("downloaded:"); //if (test.title == downloaded.title) alert("titles match:"+test.title); }); //downloadPosting }); //forEach test }); //forEach profile */ //$('#accordion',checkDlg).accordion(); checkDlg.dialog({ width: 700, height: 500, open: function () { // http://stackoverflow.com/questions/2929487/putting-a-jquery-ui-accordion-in-a-jquery-ui-dialog $('#accordion').accordion({ autoHeight: true }); } }); // show dialog } // selfCheckDialog // 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' : ''; //dbLog("value is:"+get_persistent("debug_mode_enabled")); //dbLog("persistent debug flag is:"+checked); var setupDiv = $('
NOTE: this configuration dialog is still work-in-progress

'); setupDiv.click(function () { //alert("changing persistent debug state"); Host.set_persistent('debug_mode_enabled', $('#debugcb').is(':checked')); }); //MediaWiki editing stub, based on: https://www.mediawiki.org/wiki/API:Edit#Editing_via_Ajax //only added here to show some status info in the setup dialog Host.download('http://wiki.flightgear.org/api.php?action=query&prop=info|revisions&intoken=edit&rvprop=timestamp&titles=Main%20Page', function (response) { var message = 'FlightGear wiki login status (AJAX):'; var status = response.statusText; var color = (status == 'OK') ? 'green' : 'red'; Host.dbLog(message + status); var statusDiv = $('

' + message + status + '

').css('color', color); setupDiv.append(statusDiv); }); setupDiv.dialog(); } // setupDialog // this can be used to download/cache $FG_ROOT/options.xml so that fgfs CLI arguments can be recognized automatically // which can help transforming postings correctly function downloadOptionsXML() { // download $FG_ROOT/options.xml Host.download("https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/options.xml?format=raw", function(response) { var xml = response.responseText; var doc = Host.make_doc(xml, 'text/xml'); // https://developer.mozilla.org/en-US/docs/Web/API/XPathResult var options = Host.eval_xpath(doc, '//*/option', XPathResult.ORDERED_NODE_SNAPSHOT_TYPE); // http://help.dottoro.com/ljgnejkp.php Host.dbLog("Number of options found in options.xml:"+options.snapshotLength); // http://help.dottoro.com/ljtfvvpx.php // https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/options.xml }); // end of options.xml download } // downloadOptionsXML 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 (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) { Host.dbLog("ask for config dialog to be shown"); var response = UI.confirm('This is your first time running version ' + Host.getScriptVersion() + '\nConfigure now?'); if (response) { // show configuration dialog (jQuery) setupDialog(); } else { } // don't configure } // 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); return CONFIG[profile]; } Host.dbLog('Could not find matching URL in getProfile() call!'); } }// Get the HTML code that is selected function getSelectedHtml() { // From http://stackoverflow.com/a/6668159 var html = '', selection = document.getSelection(); if (selection.rangeCount) { var container = document.createElement('div'); for (var i = 0; i < selection.rangeCount; i++) { container.appendChild(selection.getRangeAt(i).cloneContents()); } html = container.innerHTML; } Host.dbLog('instantCquote(): Unprocessed HTML\n\'' + html + '\''); return html; }// Gets the selected text function getSelectedText() { return document.getSelection().toString(); }// Get the ID of the post // (this needs some work so that it can be used by the AJAX mode, without an actual selection) function getPostId(selection, profile, focus) { if (focus !== undefined) { Host.dbLog("Trying to get PostId with defined focus"); selection = selection.focusNode.parentNode; } else { Host.dbLog("Trying to get PostId with undefined focus"); selection = selection.anchorNode.parentNode; } while (selection.id.match(profile.content.idStyle) === null) { selection = selection.parentNode; } Host.dbLog("Selection id is:"+selection.id); return selection.id; } // Checks that the selection is valid function checkValid(selection, profile) { var ret = true, selection_cp = { }, tags = profile.content.parentTag; for (var n = 0; n < 2; n++) { if (n === 0) { selection_cp = selection.anchorNode.parentNode; } else { selection_cp = selection.focusNode.parentNode; } while (true) { if (selection_cp.tagName === 'BODY') { ret = false; break; } else { var cont = false; for (var i = 0; i < tags.length; i++) { if (selection_cp[tags[0]] === tags[i]) { cont = true; break; } } if (cont) { break; } else { selection_cp = selection_cp.parentNode; } } } } ret = ret && (getPostId(selection, profile) === getPostId(selection, profile, 1)); return ret; }// Extracts the raw text from a certain place, using an XPath function extractFieldInfo(profile, id, field) { if (field === 'content') { Host.dbLog("Returning content (selection)"); return profile[field].selection(); } else { Host.dbLog("Extracting field via xpath:"+field); var xpath = '//*[@id="' + id + '"]/' + profile[field].xpath; return Host.eval_xpath(document, xpath).stringValue; // document.evaluate(xpath, document, null, XPathResult.STRING_TYPE, null).stringValue; } }// Change the text using specified transformations function applyTransformations(fieldInfo, trans) { for (var i = 0; i < trans.length; i++) { fieldInfo = trans[i](fieldInfo); Host.dbLog('applyTransformations(): Multiple transformation, transformation after loop #' + (i + 1) + ':\n\'' + fieldInfo + '\''); } return fieldInfo; } //applyTransformations // Formats the quote function createCquote(data, light_quotes = true) { // skip FGCquote (experimental) if (light_quotes) return nonQuotedRef(data); var date_added = new Date(); var wikiText = '{{FGCquote\n' + ((data.content.match(/^\s*?{{cquote/) === null) ? '|1= ' : '| ') + data.content + '\n' + '|2= ' + createCiteWeb(data) + '\n' + '}}'; return wikiText; } function nonQuotedRef(data) { return addContentBlob(data) + createRefCite(data); }// function addContentBlob(data) { return data.content; }// wrap citation in ref tags function createRefCite(data) { return '' + createCiteWeb(data) + ''; } function createCiteWeb(data) { var date_added = new Date(); var wikiText = '{{cite web\n' + ' |url = ' + data.url + '\n' + ' |title = ' + nowiki(data.title) + '\n' + ' |author = ' + nowiki(data.author) + '\n' + ' |date = ' + datef(data.date) + '\n' + ' |added = ' + datef(date_added.toDateString()) + '\n' + ' |script_version = ' + GM_info.script.version + '\n' + ' }}\n'; return wikiText; } // Output the text. // Tries the jQuery dialog, and falls back to window.prompt() function outputText(msg) { try { OUTPUT.jQueryTabbed(msg); //jQueryDiag(msg); // TODO: unify code & call Host.setClipboard() here } catch (err) { msg = msg.replace(/<\/syntaxhighligh(.)>/g, '/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, '{{!-}}'); return html.replace(/\|/g, '{{!}}'); }// Converts HTML ... tags to wiki links, internal if possible. function a2wikilink(html) { // Links to wiki images, because // they need special treatment, or else they get displayed. html = html.replace(/(.*?)<\/a>/g, '[[Media:$1|$2]]'); // Wiki links without custom text. html = html.replace(/http:\/\/wiki\.flightgear\.org\/.*?<\/a>/g, '[[$1]]'); // Links to the wiki with custom text html = html.replace(/(.*?)<\/a>/g, '[[$1|$2]]'); // Remove underscores from all wiki links var list = html.match(/\[\[.*?\]\]/g); if (list !== null) { for (var i = 0; i < list.length; i++) { html = html.replace(list[i], underscore2Space(list[i])); } } // Convert non-wiki links // TODO: identify forum/devel list links, and use the AJAX/Host.download helper to get a title/subject for unnamed links (using the existing xpath/regex helpers for that) html = html.replace(/(.*?)<\/a>/g, '[$1 $2]'); // Remove triple dots from external links. // Replace with raw URL (MediaWiki converts it to a link). list = html.match(/\[.*?(\.\.\.).*?\]/g); if (list !== null) { for (var i = 0; i < list.length; i++) { html = html.replace(list[i], list[i].match(/\[(.*?) .*?\]/) [1]); } } return html; }// Converts images, including images in links function img2link(html) { html = html.replace(/<\/a>/g, '[[File:$2|250px|link=$1]]'); html = html.replace(//g, '[[File:$1|250px]]'); html = html.replace(/<\/a>/g, '(see [$2 image], links to [$1 here])'); return html.replace(//g, '(see the [$1 linked image])'); }// Converts smilies function forum_smilies2text(html) { html = html.replace(/(.*?)/g, '$1'); for (var i = 0; i < EMOTICONS.length; i++) { html = html.replace(EMOTICONS[i][0], EMOTICONS[i][1]); } return html; }// Converts font formatting function forum_fontstyle2wikistyle(html) { html = html.replace(/(.*?)<\/span>/g, '\'\'\'$1\'\'\''); html = html.replace(/(.*?)<\/span>/g, '$1'); html = html.replace(/(.*?)<\/span>/g, '\'\'$1\'\''); return html.replace(/(.*?)<\/span>/g, '$1'); }// Converts code blocks function forum_code2syntaxhighlight(html) { var list = html.match(/
.*?(.*?)<\/code>.*?<\/dl>/g), data = [ ]; if (list === null) return html; for (var n = 0; n < list.length; n++) { data = html.match(/
.*?(.*?)<\/code>.*?<\/dl>/); html = html.replace(data[0], processCode(data)); } return html; }// Strips any whitespace from the beginning and end of a string function stripWhitespace(html) { html = html.replace(/^\s*?(\S)/, '$1'); return html.replace(/(\S)\s*?\z/, '$1'); }// Process code, including basic detection of language function processCode(data) { var lang = '', code = data[1]; code = code.replace(/ /g, ' '); if (code.match(/=?.*?\(?.*?\)?;/) !== null) lang = 'nasal'; if (code.match(/<.*?>.*?<\/.*?>/) !== null || code.match(/<!--.*?-->/) !== null) lang = 'xml'; code = code.replace(//g, '\n'); return '\n' + code + '\n</syntaxhighlight>'; }// Converts quote blocks to Cquotes function forum_quote2cquote(html) { html = html.replace(/
(.*?)<\/div><\/blockquote>/g, '{{cquote|$1}}'); if (html.match(/
/g) === null) return html; var numQuotes = html.match(/
/g).length; for (var n = 0; n < numQuotes; n++) { html = html.replace(/
(.*?) wrote.*?:<\/cite>(.*?)<\/div><\/blockquote>/, '{{cquote|$2|$1}}'); } return html; }// Converts videos to wiki style function vid2wiki(html) { // YouTube html = html.replace(/
\s.*?
\s*?