// ==UserScript== // @name Instant-Cquotes // @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) // @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. // 'use strict'; // TODO: move to GreaseMonkey host // prevent conflicts with jQuery used on webpages: https://wiki.greasespot.net/Third-Party_Libraries#jQuery this.$ = this.jQuery = jQuery.noConflict(true); // set this to true continue working on the new mode supporting // asynchronous content fetching via AJAX // var USE_NG = false; // 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) 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.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, 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); recipient.call(undefined, test); //test.call(undefined, callback) } // 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 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) // 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() } // 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(); // downloadOptionsXML(); // hash with supported websites/URLs, includes xpath and regex expressions to extract certain fields, and optional transformations for post-processing 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) 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 }, // the event handler to be invoked url_reg: '^(http|https)://wiki.flightgear.org' // ignore: not currently used by the wiki mode }, // 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 selection: getSelectedText, idStyle: /msg[0-9]{8}/, parentTag: [ 'tagName', 'PRE' ] }, // 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', title: 'Re: [Flightgear-devel] Auto altimeter setting at startup (?)' }, { url: 'https://sourceforge.net/p/flightgear/mailman/message/35059961/', author: 'Ludovic Brenta', 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', title: 'Re: [Flightgear-devel] Cockpit displays (rendering, modelling)' }, { url: 'https://sourceforge.net/p/flightgear/mailman/message/23518343/', author: 'Tim Moore', 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()' }, 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: { 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 tests: [ { url: 'https://forum.flightgear.org/viewtopic.php?f=18&p=284108#p284108', author: 'mickybadia', title: 'OSM still PNG maps' }, { url: 'https://forum.flightgear.org/viewtopic.php?f=19&p=284120#p284120', author: 'Thorsten', 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', title: 'Re: Best way to learn Canvas?' }, { url: 'https://forum.flightgear.org/viewtopic.php?f=4&t=1460&p=283994#p283994', author: 'bugman', title: 'Re: eurofighter typhoon' } // add other tests below ], // end of vector with self-tests author: { xpath: 'div/div[1]/p/strong/a/text()' }, title: { xpath: 'div/div[1]/h3/a/text()' }, 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') ] } } }; // hash to map URLs (wiki article, issue tracker, sourceforge link, forum thread etc) to existing wiki templates var URL2TemplateTable = { // placeholder for now }; // TemplateTable var EventHandlers = { updateTarget: function () { alert('not yet implement'); }, updateFormat: function () { alert('not yet implement'); } }; // EventHandlers // output methods (alert and jQuery for now) var OUTPUT = { // Shows a window.prompt() message box msgbox: function (msg) { window.prompt('Copy to clipboard ' + Host.getScriptVersion(), msg); Host.setClipboard(msg); }, // Show a jQuery dialog 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 = '
Target: Format:
'; //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 = $('
' + target_format + '
'); var diagParam = { title: 'Copy your quote with Ctrl+c ' + Host.getScriptVersion(), modal: true, width: 'auto', buttons: [ /* { 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); } }; ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// var speechTransformations = [ // 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 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) {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 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') 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); } // foreach test } // foreach profile (website) } //runProfileTests function selfCheckDialog() { var sections = '

Important APIs:

(to be added here using the Environment.runAPITests() helper)
'; try { runProfileTests.call(undefined); // check website profiles } catch (e) { 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); }); }); //$('#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 = 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) { if (typeof trans === 'function') { return trans(fieldInfo); } else if (Array.isArray(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; } }// 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.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*?