Instant-Cquotes

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        Instant-Cquotes
// @name:it     Instant-Cquotes
// @license     public domain
// @version     0.39
// @date        2016-05-20
// @description Automatically converts selected FlightGear mailing list and forum quotes into post-processed MediaWiki markup (i.e. cquotes).
// @description:it Converte automaticamente citazioni dalla mailing list e dal forum di FlightGear in marcatori MediaWiki (cquote).
// @author      Hooray, bigstones, Philosopher, Red Leader & Elgaton (2013-2016)
// @supportURL  http://wiki.flightgear.org/FlightGear_wiki:Instant-Cquotes
// @icon        http://wiki.flightgear.org/images/2/25/Quotes-logo-200x200.png
// @match       https://sourceforge.net/p/flightgear/mailman/*
// @match       http://sourceforge.net/p/flightgear/mailman/*
// @match       https://forum.flightgear.org/*
// @match       http://wiki.flightgear.org/*
// @namespace   http://wiki.flightgear.org/FlightGear_wiki:Instant-Cquotes
// @run-at      document-start
// @require     https://code.jquery.com/jquery-1.10.2.js
// @require     https://code.jquery.com/ui/1.11.4/jquery-ui.js
// @require     https://cdn.jsdelivr.net/genetic.js/0.1.14/genetic.js
// @require     https://cdn.jsdelivr.net/synaptic/1.0.4/synaptic.min.js
// @resource    jQUI_CSS https://code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css
// @resource    myLogo http://wiki.flightgear.org/images/2/25/Quotes-logo-200x200.png
// @grant       GM_registerMenuCommand
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_addStyle
// @grant       GM_getResourceText
// @grant       GM_getResourceURL
// @grant       GM_setClipboard
// @grant       GM_xmlhttpRequest
// @noframes
// ==/UserScript==
//
// This work has been released into the public domain by their authors. This
// applies worldwide.
// In some countries this may not be legally possible; if so:
// The authors grant anyone the right to use this work for any purpose, without
// any conditions, unless such conditions are required by law.
//
// This script has a number of dependencies that are implicitly satisfied when run as a user script 
// via GreaseMonkey/TamperMonkey; however, these need to be explicitly handled when using a different mode (e.g. firefox/android):
// 
// - jQuery - user interface (REQUIRED)
// - genetic-js - genetic programming (OPTIONAL/EXPERIMENTAL)
// - synaptic - neural networks (OPTIONAL/EXPERIMENTAL)
// 
// 

/* Here are some TODOs
 * - support RSS feeds http://dir.gmane.org/gmane.games.flightgear.devel/
 * - move event handling/processing to the CONFIG hash
 * - use try/catch more widely
 * - wrap function calls in try/call for better debugging/diagnostics
 * - add helpers for [].forEach.call, map, apply and call
 * - replace for/in, for/of, let statements for better compatibility (dont require ES6)
 * - for the same reason, replace use of functions with default params 
 * - isolate UI (e.g. JQUERY) code in UserInterface hash
 * - expose regex/transformations via the UI
 *
 */

'use strict';


// TODO: move to GreaseMonkey/UI host
// prevent conflicts with jQuery used on webpages: https://wiki.greasespot.net/Third-Party_Libraries#jQuery
// http://stackoverflow.com/a/5014220
this.$ = this.jQuery = jQuery.noConflict(true);

// this hash is just intended to help isolate UI specifics
// so that we don't need to maintain/port tons of code 

var UserInterface = {
  get: function() {
    return UserInterface.DEFAULT;
  },
  
 CONSOLE: {
   
 }, // CONSOLE (shell, mainly useful for testing)
  
 DEFAULT: {
  alert: function(msg) {return window.alert(msg);     },
  prompt: function(msg) {return window.prompt(msg);  }, 
  confirm: function(msg) {return window.confirm(msg); },
  dialog: null,
  selection: null,
  populateWatchlist: function() {
    
  },
  populateEditSections: function() {
    
  }
 
 }, // default UI mapping (Browser/User script)
  
  JQUERY: {
    
  } // JQUERY 
  
}; // UserInterface

var UI = UserInterface.get(); // DEFAULT for now


// This hash is intended to help encapsulate platform specifics (browser/scripting host)
// Ideally, all APIs that are platform specific should be kept here
// This should make it much easier to update/port and maintain the script in the future
var Environment = {
  getHost: function(xpi=false) {
 
     if(xpi) {
       Environment.scriptEngine = 'firefox addon';
       console.log('in firefox xpi/addon mode');
       return Environment.FirefoxAddon; // HACK for testing the xpi mode (firefox addon)
     }
    
    // This will determine the script engine in use: http://stackoverflow.com/questions/27487828/how-to-detect-if-a-userscript-is-installed-from-the-chrome-store
    if (typeof(GM_info) === 'undefined') {
    Environment.scriptEngine = "plain Chrome (Or Opera, or scriptish, or Safari, or rarer)";
    // See http://stackoverflow.com/a/2401861/331508 for optional browser sniffing code.
   }
   else {
    Environment.scriptEngine = GM_info.scriptHandler  ||  "Greasemonkey";
    }
   console.log ('Instant cquotes is running on ' + Environment.scriptEngine + '.');
    
   //console.log("not in firefox addon mode...");
    // See also: https://wiki.greasespot.net/Cross-browser_userscripting
    return Environment.GreaseMonkey; // return the only/default host (for now)
  },
  
  validate: function(host) {
    if (host.get_persistent('startup.disable_validation',false)) return;
    
    if(Environment.scriptEngine !== "Greasemonkey") 
      console.log("NOTE: This script has not been tested with script engines other than GreaseMonkey recently!");
    
    var dependencies = [
      {name:'jQuery', test: function() {} },
      {name:'genetic.js', test: function() {} },
      {name:'synaptic', test: function() {} },
    ];
    
    [].forEach.call(dependencies, function(dep) {
      console.log("Checking for dependency:"+dep.name);
      var status=false;
      try {
      dep.test.call(undefined);
      status=true;
      }
      catch(e) {
      status=false;       
      }
      finally {
        var success = (status)?'==> success':'==> failed';
        console.log(success);
        return status;
      }
    });
  }, // validate
  
  // this contains unit tests for checking crucial APIs that must work for the script to work correctly
  // for the time being, most of these are stubs waiting to be filled in
  // for a working example, refer to the JSON test at the end
  // TODO: add jQuery tests
  APITests: [
     {name:'download', test: function(recipient) {recipient(true);}  },
     {name:'make_doc', test: function(recipient) { recipient(true);}   },
     {name:'eval_xpath', test: function(recipient) { recipient(true);} },
     {name:'JSON de/serialization', test: function(recipient) {
       //console.log("running json test");
       var identifier = 'unit_tests.json_serialization';
       var hash1 = {x:1,y:2,z:3};
       Host.set_persistent(identifier, hash1, true);
       var hash2 = Host.get_persistent(identifier,null,true);
       
       recipient(JSON.stringify(hash1) === JSON.stringify(hash2));
     } // callback 
     },
    
    // downloads a posting and tries to transform it to 3rd person speech ...
    // TODO: add another test to check forum postings
    {name:'text/speech transformation', test: function(recipient) {
    
    // the posting we want to download
    var url='https://sourceforge.net/p/flightgear/mailman/message/35066974/';
    Host.downloadPosting(url, function (result) {
      
    // only process the first sentence by using comma/dot as delimiter
    var firstSentence = result.content.substring(result.content.indexOf(',')+1, result.content.indexOf('.'));
      
    var transformed = transformSpeech(firstSentence, result.author, null, speechTransformations );
    console.log("3rd person speech transformation:\n"+transformed);   
    
    recipient(true);
    }); // downloadPosting() 
        
  }// test()
    }, // end of speech transform test
    {
      name:"download $FG_ROOT/options.xml", test: function(recipient) {
        downloadOptionsXML();
        recipient(true);
      } // test
    }
    
  ], // end of APITests
  
  runAPITests: function(host, recipient) {
    console.log("Running API tests");
    for(let test of Environment.APITests ) {
      //var test = Environment.APITests[t];
      // invoke the callback passed, with the hash containing the test specs, so that the console/log or a div can be updated showing the test results
      
      recipient.call(undefined, test);
      
    } // foreach test
  }, // runAPITests
  
  /*
   * ===================================================================================================================================================
   *
   */
  
  // NOTE: This mode/environment is WIP and highly experimental ...
  // To see this working, you need to package up the whole file as a firefox xpi using "jpm xpi"
  // and then start the whole thing via "jpm run", to do that, you also need a matching package.json (i.e. via jpm init) 
  // ALSO: you will have to explicitly install any dependencies using jpm
  FirefoxAddon: {
  	init: function() {
		console.log("Firefox addon mode ...");
  	},
	getScriptVersion: function() {
		return '0.36'; // FIXME
	},
	dbLog: function(msg) {
		console.log(msg);
	},
	addEventListener: function(ev, cb) {

	require("sdk/tabs").on("ready", logURL);
 	function logURL(tab) {
  		console.log("URL loaded:" + tab.url);
	}	
	},
    
	registerConfigurationOption: function(name, callback, hook) {
	// https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Add_a_Context_Menu_Item
		console.log("config menu support n/a in firefox mode");
 // https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Using_third-party_modules_%28jpm%29  
 var menuitems = require("menuitem");
 var menuitem = menuitems.Menuitem({
  id: "clickme",
  menuid: "menu_ToolsPopup",
  label: name,
  onCommand: function() {
    console.log("menuitem clicked:");
    callback();
  },
  insertbefore: "menu_pageInfo"
});
	},
    
	registerTrigger: function() {
		// https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Add_a_Context_Menu_Item
		// https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/context-menu#Item%28options%29
		var contextMenu = require("sdk/context-menu");
		var menuItem = contextMenu.Item({
  		label: "Instant Cquote",
  		context: contextMenu.SelectionContext(),
      // https://developer.mozilla.org/en/Add-ons/SDK/Guides/Two_Types_of_Scripts
      // https://developer.mozilla.org/en-US/Add-ons/SDK/Guides/Content_Scripts
  		contentScript: 'self.on("click", function () {' +
                 '  var text = window.getSelection().toString();' +
                 '  self.postMessage(text);' +
                 '});',
  		onMessage: function (selectionText) {
    		console.log(selectionText);
        instantCquote(selectionText);
  		}
	});
  
    // for selection handling stuff, see: https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/selection
    
    function myListener() {
  console.log("A selection has been made.");
}
var selection = require("sdk/selection");
selection.on('select', myListener);
    
	}, //registerTrigger
    
	get_persistent: function(key, default_value) {
    // https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/simple-storage
    var ss = require("sdk/simple-storage");
    
    console.log("firefox mode does not yet have persistence support");
    return default_value;},
	set_persistent: function(key, value) {
		console.log("firefox persistence stubs not yet filled in !");
	},
    
  
	set_clipboard: function(content) {
	// https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/clipboard
    
	//console.log('clipboard stub not yet filled in ...');
    var clipboard = require("sdk/clipboard");
    clipboard.set(content);
	} //set_cliipboard
    
  }, // end of FireFox addon config
  
  // placeholder for now ...
  Android: {
    // NOP
  }, // Android

  
  ///////////////////////////////////////
  // supported  script engines:
  ///////////////////////////////////////
  
  GreaseMonkey: {
  // TODO: move environment specific initialization code here  
  init: function() {
  // Check if Greasemonkey/Tampermonkey is available
  try {
  // TODO: add version check for clipboard API and check for TamperMonkey/Scriptish equivalents ?
  GM_addStyle(GM_getResourceText('jQUI_CSS'));
  } // try
  catch (error) {
  console.log('Could not add style or determine script version');
  } // catch

  var commands = [
  {name:'Setup quotes',callback:setupDialog, hook:'S' },
  {name:'Check quotes',callback:selfCheckDialog, hook:'C' }
  ];
      
  for (let c of commands ) {
   this.registerConfigurationOption(c.name, c.callback, c.hook);
  }  
     
  }, // init()
    
  getScriptVersion: function() {
  return GM_info.script.version;  
  },
    
  dbLog: function (message) {
  if (Boolean(DEBUG)) {
    console.log('Instant cquotes:' + message);
  }
  }, // dbLog()
    
  registerConfigurationOption: function(name,callback,hook) {
  // https://wiki.greasespot.net/GM_registerMenuCommand
  // https://wiki.greasespot.net/Greasemonkey_Manual:Monkey_Menu#The_Menu
    GM_registerMenuCommand(name, callback, hook);
  }, //registerMenuCommand()
    
  registerTrigger: function() {
    
    // TODO: we can use the following callback non-interactively, i.e. to trigger background tasks
// http://javascript.info/tutorial/onload-ondomcontentloaded
document.addEventListener("DOMContentLoaded", function(event) {
    console.log("Instant Cquotes: DOM fully loaded and parsed");
});

window.addEventListener('load', init); // page fully loaded
Host.dbLog('Instant Cquotes: page load handler registered');

    
    // Initialize (matching page loaded)
function init() {
  console.log('Instant Cquotes: page load handler invoked');
  var profile = getProfile();
  
  Host.dbLog("Profile type is:"+profile.type);
  
  // Dispatch to correct event handler (depending on website/URL)
  // TODO: this stuff could/should be moved into the config hash itself
  
  if (profile.type=='wiki') {
    profile.event_handler(); // just for testing
    return;
  }
   
    Host.dbLog('using default mode');
    document.onmouseup = instantCquote;
    // HACK: preparations for moving the the event/handler logic also into the profile hash, so that the wiki (edit mode) can be handled equally
    //eval(profile.event+"=instantCquote");
     
} // init()


    
  }, // registerTrigger

    
   download: function (url, callback, method='GET') {
  // http://wiki.greasespot.net/GM_xmlhttpRequest
     try {
  GM_xmlhttpRequest({
    method: method,
    url: url,
    onload: callback
  });
     }catch(e) {
       console.log("download did not work");
     }
  }, // download()
    
    // is only intended to work with archives supported by the  hash
    downloadPosting: function (url, EventHandler) {
      
    Host.download(url, function (response) {
    var profile = getProfile(url);
    var blob = response.responseText;
    var doc = Host.make_doc(blob,'text/html'); 
    var result = {}; // hash to be returned
    
    [].forEach.call(['author','date','title','content'], function(field) {
      var xpath_query = '//' + profile[field].xpath;
      try {
       var value = Host.eval_xpath(doc, xpath_query).stringValue; 
       //UI.alert("extracted field value:"+value);
        
        // now apply all transformations, if any
       value = applyTransformations(value, profile[field].transform );
        
       result[field]=value; // store the extracted/transormed value in the hash that we pass on
      } // try
      catch(e) {
        UI.alert("downloadPosting failed:\n"+ e.message);
      } // catch
    }); // forEach field
    
    EventHandler(result); // pass the result to the handler
    }); // call to Host.download() 
      
    }, // downloadPosting()
    
    // TODO: add makeAJAXCall, and makeWikiCall here

  
    // turn a string/text blob into a DOM tree that can be queried (e.g. for xpath expressions)
    // FIXME: this is browser specific not GM specific ...
    make_doc: function(text, type='text/html') {
      // to support other browsers, see: https://developer.mozilla.org/en/docs/Web/API/DOMParser
      return new DOMParser().parseFromString(text,type);
    }, // make DOM document
    
    // xpath handling may be handled separately depending on browser/platform, so better encapsulate this
    // FIXME: this is browser specific not GM specific ...
    eval_xpath: function(doc, xpath, type=XPathResult.STRING_TYPE) {
      return doc.evaluate(xpath, doc, null, type, null);
    }, // eval_xpath
    
    set_persistent: function(key, value, json=false) 
    {
      // transparently stringify to json
      if(json) {
        // http://stackoverflow.com/questions/16682150/store-a-persistent-list-between-sessions
        value = JSON.stringify (value);
      }
      
      // https://wiki.greasespot.net/GM_setValue
      GM_setValue(key, value);
      //UI.alert('Saved value for key\n'+key+':'+value);
    }, // set_persistent
    
    get_persistent: function(key, default_value, json=false) {
     // https://wiki.greasespot.net/GM_getValue
    
      var value=GM_getValue(key, default_value);
      // transparently support JSON: http://stackoverflow.com/questions/16682150/store-a-persistent-list-between-sessions
      if(json) {
        value = JSON.parse (value)  ||  {};
      }
      return value;
    }, // get_persistent

   setClipboard: function(msg) {
   // this being a greasemonkey user-script, we are not 
   // subject to usual browser restrictions
   // http://wiki.greasespot.net/GM_setClipboard
   GM_setClipboard(msg);
  }, // setClipboard()
    
    getTemplate: function() {
    
    // hard-coded default template
    var template = '$CONTENT<ref>{{cite web\n' +
  '  |url    =  $URL \n' +
  '  |title  =  <nowiki> $TITLE </nowiki> \n' +
  '  |author =  <nowiki> $AUTHOR </nowiki> \n' +
  '  |date   =  $DATE \n' +
  '  |added  =  $ADDED \n' +
  '  |script_version = $SCRIPT_VERSION \n' +
  '  }}</ref>\n';
     
    // return a saved template if found, fall back to hard-coded one above otherwise
    return Host.get_persistent('default_template', template);
    
  } // getTemplate

    
  } // end of GreaseMonkey environment, add other environments below
  
}; // Environment hash - intended to help encapsulate host specific stuff (APIs)


// the first thing we need to do is to determine what APIs are available
// and store everything in a Host hash, which is subsequently used for API lookups
// the Host hash contains all platform/browser-specific APIs
var Host = Environment.getHost();
Environment.validate(Host); // this checks the obtained host to see if all required dependencies are available
Host.init(); // run environment specific initialization code (e.g. logic for GreaseMonkey setup)


// move DEBUG handling to a persistent configuration flag so that we can configure this using a jQuery dialog (defaulted to false)
// TODO: move DEBUG variable to Environment hash / init() routine
var DEBUG = Host.get_persistent('debug_mode_enabled', false);
Host.dbLog("Debug mode is:"+DEBUG);
function DEBUG_mode() {
  // reset script invocation counter for testing purposes
  Host.dbLog('Resetting script invocation counter');
  Host.set_persistent(GM_info.script.version, 0);
}


if (DEBUG)
DEBUG_mode();

// hash with supported websites/URLs,  includes xpath and regex expressions to extract certain fields, and a vector with optional transformations for post-processing each field

var CONFIG = {
  // WIP: the first entry is special, i.e. it's not an actual list archive (source), but only added here so that the same script can be used
  // for editing the FlightGear wiki
  
  'FlightGear.wiki': {
    type: 'wiki',
    enabled: false,
    event: 'document.onmouseup', // when to invoke the event handler
    // TODO: move downloadWatchlist() etc here
    event_handler: function () {
      console.log('FlightGear wiki handler active (waiting to be populated)');
      // this is where the logic for a wiki mode can be added over time (for now, it's a NOP)
    
    //for each supported mode, invoke the trigger and call the corresponding handler
    [].forEach.call(CONFIG['FlightGear.wiki'].modes, function(mode) {
      //dbLog("Checking trigger:"+mode.name);
      if(mode.trigger() ) {
        mode.handler();
      }
    });
      
    }, // the event handler to be invoked
    url_reg: '^(http|https)://wiki.flightgear.org', // ignore for now: not currently used by the wiki mode
    
    modes: [
      { name:'process-editSections',
        trigger: function() {return true;}, // match URL regex - return true for always match
       
        // the code implementing the mode
        handler: function() {
                
    var editSections = document.getElementsByClassName('mw-editsection');
    console.log('FlightGear wiki article, number of edit sections: '+editSections.length);
   
    // for now, just rewrite edit sections and add a note to them
   
     [].forEach.call(editSections, function (sec) {
       sec.appendChild(
         document.createTextNode(' (instant-cquotes is lurking) ')
       );
     }); //forEach section
        } // handler
       
       
      } // process-editSections
      // TODO: add other wiki modes below 
      
    ] // modes
    
  }, // end of wiki profile
  
  'Sourceforge Mailing list': {
    enabled: true,
    type: 'archive',
    event: 'document.onmouseup', // when to invoke the event handler
    event_handler: instantCquote, // the event handler to be invoked
    url_reg: '^(http|https)://sourceforge.net/p/flightgear/mailman/.*/',
    content: {
      xpath: 'tbody/tr[2]/td/pre/text()', // NOTE this is only used by the downloadPosting  helper to retrieve the posting without having a selection (TODO:add content xpath to forum hash)
      selection: getSelectedText,
      idStyle: /msg[0-9]{8}/,
      parentTag: [
        'tagName',
        'PRE'
      ],
      transform: [],
    }, // content recipe
    // vector with tests to be executed for sanity checks (unit testing)
    tests: [
      {
        url: 'https://sourceforge.net/p/flightgear/mailman/message/35059454/',
        author: 'Erik Hofman',
        date: 'May 3rd, 2016', // NOTE: using the transformed date here 
        title: 'Re: [Flightgear-devel] Auto altimeter setting at startup (?)'
      },
      {
        url: 'https://sourceforge.net/p/flightgear/mailman/message/35059961/',
        author: 'Ludovic Brenta',
        date: 'May 3rd, 2016',
        title: 'Re: [Flightgear-devel] dual-control-tools and the limit on packet size'
      },
      {
        url: 'https://sourceforge.net/p/flightgear/mailman/message/20014126/',
        author: 'Tim Moore',
        date: 'Aug 4th, 2008',
        title: 'Re: [Flightgear-devel] Cockpit displays (rendering, modelling)'
      },
      {
        url: 'https://sourceforge.net/p/flightgear/mailman/message/23518343/',
        author: 'Tim Moore',
        date: 'Sep 10th, 2009',
        title: '[Flightgear-devel] Atmosphere patch from John Denker'
      } // add other tests below

    ], // end of vector with self-tests
    // regex/xpath and transformations for extracting various required fields
    author: {
      xpath: 'tbody/tr[1]/td/div/small/text()',
      transform: [extract(/From: (.*) <.*@.*>/)]
    },
    title: {
      xpath: 'tbody/tr[1]/td/div/div[1]/b/a/text()',
      transform:[]
    },
    date: {
      xpath: 'tbody/tr[1]/td/div/small/text()',
      transform: [extract(/- (.*-.*-.*) /)]
    },
    url: {
      xpath: 'tbody/tr[1]/td/div/div[1]/b/a/@href',
      transform: [prepend('https://sourceforge.net')]
    }
  }, // end of mailing list profile
  // next website/URL (forum)
  'FlightGear forum': {
    enabled: true,
    type: 'archive',
    event: 'document.onmouseup', // when to invoke the event handler (not used atm)
    event_handler: null, // the event handler to be invoked (not used atm)
    url_reg: /https:\/\/forum\.flightgear\.org\/.*/,
    content: {
      xpath: '', //TODO: this must be added for downloadPosting() to work, or it cannot extract contents
      selection: getSelectedHtml,
      idStyle: /p[0-9]{6}/,
      parentTag: [
        'className',
        'content',
        'postbody'
      ],
      transform: [
        removeComments,
        forum_quote2cquote,
        forum_smilies2text,
        forum_fontstyle2wikistyle,
        forum_code2syntaxhighlight,
        img2link,
        a2wikilink,
        vid2wiki,
        list2wiki,
        forum_br2newline
      ]
    },
    // vector with tests to be executed for sanity checks (unit testing)
    // postings will be downloaded using the URL specified, and then the author/title 
    // fields extracted using the outer regex and matched against what is expected
    // NOTE: forum postings can be edited, so that these tests would fail - thus, it makes sense to pick locked topics/postings for such tests
    tests: [
      {
        url: 'https://forum.flightgear.org/viewtopic.php?f=18&p=284108#p284108',
        author: 'mickybadia',
        date: 'May 3rd, 2016',
        title: 'OSM still PNG maps'
      },
      {
        url: 'https://forum.flightgear.org/viewtopic.php?f=19&p=284120#p284120',
        author: 'Thorsten',
        date: 'May 3rd, 2016',
        title: 'Re: FlightGear\'s Screenshot Of The Month MAY 2016'
      },
       {
        url: 'https://forum.flightgear.org/viewtopic.php?f=71&t=29279&p=283455#p283446',
        author: 'Hooray',
         date: 'Apr 25th, 2016',
        title: 'Re: Best way to learn Canvas?'
      },
      {
        url: 'https://forum.flightgear.org/viewtopic.php?f=4&t=1460&p=283994#p283994',
        author: 'bugman',
        date: 'May 2nd, 2016',
        title: 'Re: eurofighter typhoon'
      } // add other tests below

    ], // end of vector with self-tests
    author: {
      xpath: 'div/div[1]/p/strong/a/text()',
      transform: [] // no transformations applied
    },
    title: {
      xpath: 'div/div[1]/h3/a/text()',
      transform: [] // no transformations applied
    },
    date: {
      xpath: 'div/div[1]/p/text()[2]',
      transform: [extract(/» (.*?[0-9]{4})/)]
    },
    url: {
      xpath: 'div/div[1]/p/a/@href',
      transform: [
        extract(/\.(.*)/),
        prepend('https://forum.flightgear.org')
      ] // transform vector
    } // url
  } // forum 
}; // CONFIG has

// hash to map URLs (wiki article, issue tracker, sourceforge link, forum thread etc) to existing wiki templates
var MatchURL2Templates = [
  // placeholder for now
 {
   name: 'rewrite sourceforge code links',
   url_reg: '',
   handler: function() {
   
 } // handler
  
 } // add other templates below
  
]; // MatchURL2Templates




// output methods (alert and jQuery for now)
var OUTPUT = {
  // Shows a window.prompt() message box
  msgbox: function (msg) {
    UI.prompt('Copy to clipboard ' + Host.getScriptVersion(), msg);
    Host.setClipboard(msg);
  }, // msgbox
  
  // this is currently work-in-progress, and will need to be refactored sooner or later
  // for now, functionality matters more than elegant design/code :)
  jQueryTabbed: function(msg, original) {
  // FIXME: using backtics here makes the whole thing require ES6  ....
  var markup = $(`<div id="tabs">
  <ul>
    <li><a href="#selection">Selection</a></li>
    <li><a href="#articles">Articles</a></li>
    <li><a href="#templates">Templates</a></li>
    <li><a href="#development">Development</a></li>
    <li><a href="#settings">Settings</a></li>
    <li><a href="#help">Help</a></li>
    <li><a href="#about">About</a></li>
  </ul>
  <div id="selection">This tab contains your extracted and post-processed selection, converted to proper wikimedia markup, including proper attribution.
  <div id="content">

    <label for="template_select">Select a template</label>
    <select name="template_select" id="template_select">
    <option>default</option>
    <option>cquote</option>
    </select>

  </div>
  <div id="options">
    <b>Note this is work-in-progress, i.e. not yet fully functional</b><br/>
    <label for="article_select">Select an article to update</label>
    <select name="article_select" id="article_select">
     <optgroup id="news" label="News"/>
     <optgroup id="support" label="Support"/>
     <optgroup id="release" label="Release"/>
     <optgroup id="develop" label="Development"/>
     <optgroup id="watchlist" label="Watchlist"/>
    </select>
    <p/>
    <label for="section_select">Select section:</label>
    <select name="section_select" id="section_select">
    </select>
  </div>
  </div>
  <div id="articles">This tab contains articles that you can directly access/edit using the mediawiki API<br/>
  Note: The watchlist is retrieved dynamically, so does not need to be edited here<br/>
    <label for="article_select">Select an article</label>
    <select name="article_select" id="article_select">
     <optgroup id="news" label="News"/>
     <optgroup id="support" label="Support"/>
     <optgroup id="develop" label="Development"/>
     <optgroup id="release" label="Release"/>
    <!-- the watchlist is retrieved dynamically, so omit it here 
     <optgroup id="watchlist" label="Watchlist"/>
    -->
    </select>

   <button id="article_new">New</button>
   <button id="article_remove">Remove</button>

  <div id="edit_article">
    <label for="article_name">Article</label>
    <input type="text" id="article_name" name="article_name"><br/>

    <label for="article_url">Link</label>
    <input type="text" id="article_url" name="article_url"><br/>

    <button id="article_save">Save</button>
  </div>

  </div>
  <div id="templates">This tab contains templates for different types of articles (newsletter, changelog, release plan etc)<p/>
  For now, this is WIP - in the future, there will be a dropdown menu added and all templates will be editable.<p/>
  <div id="template_header">

    <label for="template_select">Select a template</label>
    <select name="template_select" id="template_select">
    <option>default</option>
    <option>cquote</option>
    </select>

  </div>
  <div id="template_area"/>
  <div id="template_controls">
    <button id="template_save">Save</button>
  </div>
  </div>
  <div id="development">This tab is a placeholder for features currently under development<p/>
  <button id="evolve_regex">Evolve regex</button><p/>
  <button id="test_perceptron">Test Perceptron</button><p/>
  <div id="output">

<table id="results">
<thead>
  <tr>
     <th>Generation</th>
     <th>Fitness</th>
     <th>Expression</th>
     <th>Result</th>
  </tr>
  </thead>
  <tbody>
  </tbody>
</table> 

   <!--
   <textarea id="devel_output" lines="10"></textarea><p/>
  -->
  </div>
  </div>

  <div id="settings">This tab will contain script specific settings
  </div>
  <div id="help">One day, this tab may contain help....<p/><button id="helpButton">Instant Cquotes</button>
  </div>
  <div id="about">show some  script related information here
  </div>
</div>`); // tabs div
    
   var evolve_regex = $('div#development button#evolve_regex', markup);
   evolve_regex.click(function() {
     //alert("Evolve regex");
     evolve_expression_test();
   });
    
   var test_perceptron = $('div#development button#test_perceptron', markup);
   test_perceptron.click(function() {
     alert("Test perceptron");
   });
   
    
    // add dynamic elements to each tab
    
   // NOTE: this affects all template selectors, on all tabs
   $('select#template_select', markup).change(function() {
     UI.alert("Sorry, templates are not yet fully implemented (WIP)");
   });
    
   var help = $('#helpButton', markup);
   help.button();
   help.click(function() {
     window.open("http://wiki.flightgear.org/FlightGear_wiki:Instant-Cquotes");
   });
    
   // rows="10"cols="80" style=" width: 420px; height: 350px"
   var textarea = $('<textarea id="quotedtext" rows="20" cols="70"/>');
   textarea.val(msg);
   $('#selection #content', markup).append(textarea);
  
   var templateArea = $('<textarea id="template-edit" rows="20" cols="70"/>');
   templateArea.val( Host.getTemplate() );
   $('div#templates div#template_area', markup).append(templateArea);
   
   //$('#templates', markup).append($('<button>'));
    $('div#templates div#template_controls button#template_save',markup).button().click(function() {
      //UI.alert("Saving template:\n"+templateArea.val() );
      
      Host.set_persistent('default_template',templateArea.val() );
    }); // save template
    
  // TODO: Currently, this is hard-coded, but should be made customizable via the "articles" tab at some point ...
  var articles = [
    // NOTE: category must match an existing <optgroup> above, title must match an existing wiki article
    {category:'support', name:'Frequently asked questions', url:''},
    {category:'support', name:'Asking for help', url:''},
    {category:'news', name:'Next newsletter', url:''},
    {category:'news', name:'Next changelog', url:''},
    {category:'release', name:'Release plan/Lessons learned', url:''}, // TODO: use wikimedia template
    {category:'develop', name:'Nasal library', url:''},
    {category:'develop', name:'Canvas Snippets', url:''},
    
  ];
    
    // TODO: this should be moved elsewhere
    function updateArticleList(selector) {
    $.each(articles, function (i, article) {
    $(selector+ ' optgroup#'+article.category, markup).append($('<option>', { 
        value: article.name, // FIXME: just a placeholder for now
        text : article.name 
    })); //append option
   }); // foreach
    } // updateArticleList
    
    // add the article list to the corresponding dropdown menus
    updateArticleList('select#article_select');
        
    // populate watchlist (prototype for now)
    // TODO: generalize & refactor: url, format
      
    // https://www.mediawiki.org/wiki/API:Watchlist
    // http://wiki.flightgear.org/api.php?action=query&list=watchlist
      var watchlist_url = 'http://wiki.flightgear.org/api.php?action=query&list=watchlist&format=json';
      Host.download(watchlist_url, function(response) {
        try {
       var watchlist = JSON.parse(response.responseText);
            
       //$('div#options select#section_select', markup).empty(); // delete all sections
      
      $.each(watchlist.query.watchlist, function (i, article) {
      $('div#options select#article_select optgroup#watchlist', markup).append($('<option>', { 
        value: article.title, //FIXME just a placeholder for now
        text : article.title 
    }));
   }); //foreach section

        }
        catch (e) {
          UI.alert(e.message);
        }
      }); // download & populate watchlist
      
    
    // register an event handler for the main tab, so that article specific sections can be retrieved
    $('div#options select#article_select', markup).change(function() {
      var article = this.value;
      
    // HACK: try to get a login token (actually not needed just for reading ...)
    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;
    
    // populate dropdown menu with article sections
    if (status === 'OK') {
    
      // Resolve redirects: https://www.mediawiki.org/wiki/API:Query#Resolving_redirects
      var section_url = 'http://wiki.flightgear.org/api.php?action=parse&page='+encodeURIComponent(article)+'&prop=sections&format=json&redirects';
      Host.download(section_url, function(response) {
        try {
       var sections = JSON.parse(response.responseText);
            
       $('div#options select#section_select', markup).empty(); // delete all sections
      
      $.each(sections.parse.sections, function (i, section) {
      $('div#options select#section_select', markup).append($('<option>', { 
        value: section.line, //FIXME just a placeholder for now
        text : section.line 
    }));
   }); //foreach section

        }
        catch (e) {
          UI.alert(e.message);
        }
             
      }); //download sections
     
      
      
    } // login status is OK

      
  }); // Host.download() call, i.e. we have a login token
      
    }); // on select change
    
  // init the tab stuff
  markup.tabs();
  
  var diagParam = {
      title: 'Instant Cquotes ' + Host.getScriptVersion(),
      modal: true,
      width: 700,
      buttons: [
        {
          text:'reported speech',
          click: function() {
            textarea.val(createCquote(original,true));
          }
        },
        
        {
          text: 'Copy',
          click: function () {
            Host.setClipboard(msg);
            $(this).dialog('close');
          }
        }
        
      ]
  };
    
  // actually show our tabbed dialog using the params above
  markup.dialog(diagParam);
    
    
  } // jQueryTabbed() 
  
}; // output methods

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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 using vectors: would/should 
// ordering is crucial here (most specific first, least specific/most generic last)
 
// first, we start off  by expanding short forms: http://www.learnenglish.de/grammar/shortforms.html
// http://www.macmillandictionary.com/thesaurus-category/british/short-forms
 
  {query:/couldn\'t/gi, replacement:'could not'},
  {query:/I could not/gi, replacement:'$author could not'},
  
  {query:/I\'m/gi, replacement:'I am'},
  {query:/I am/gi, replacement:'$author is'},
  
  {query:/I\'ve/, replacement:'I have'},
  {query:/I have had/, replacement:'$author had'},
  
  
  {query:/can(\'|\’)t/gi, replacement:'cannot'},
  
  {query:/I(\'|\’)ll/gi, replacement:'$author will'},
  {query:/I(\'|\’)d/gi, replacement:'$author would'},
  
  {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 believe/gi, replacement:'$author suggested'},
  {query:/I think/gi, replacement:'$author suggested'},
  {query:/I guess/gi, replacement:'$author believes'},
  
  {query:/I can see that/gi, replacement:'$author suggested that'},
  
  
  {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\’m prototyping/gi, replacement:'$author is prototyping'},
  
  {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)
  //alert("text to be transformed:\n"+text);
  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 #
// ##################


// the required trigger is host specific (userscript vs. addon vs. android etc)
// for now, this merely wraps window.load mapping to the instantCquotoe callback below
Host.registerTrigger();


// FIXME: function is currently referenced in CONFIG hash - event_handler, so cannot be easily moved across
// The main function
// TODO: split up, so that we can reuse the code elsewhere
function instantCquote(sel) {
  var profile = getProfile();
  
  // TODO: use config hash here
  var selection =  document.getSelection(),
  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;
  }
  try {
    transformationLoop(profile, post_id);
  }
  catch(e) {
    UI.alert("Transformation loop:\n"+e.message);
  }
} // instantCquote

  // 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)
function transformationLoop(profile, post_id) {
  var output = {}, field;
  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);
  
  var outputPlain = createCquote(output);
  outputText(outputPlain, output);
} // transformationLoop()



/// #############

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
    // should be really moved to downloadPostign
    if (CONFIG[profile].content.xpath === '') console.log("xpath for content extraction is empty, cannot procedurally extract contents");
    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 = '<h3>Important APIs:</h3><div id="api_checks"></div>';


  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 + '<p/>';
    }
    sections +='<h3>' + profile + ':<font color="blue">'+ CONFIG[profile].url_reg+'</font></h3><div><p>' + test_results + '</p></div>\n';
  }  // https://jqueryui.com/accordion/
  
 
  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);
  }); // 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 = $('<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>');
  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 = $('<p>' + message + status + '</p>').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 and post-processed 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)
      // FIXME: this is triggered/incremented by each click ...
      Host.dbLog("increment number of script invocations");
      Host.set_persistent(Host.getScriptVersion(), invocations + 1);
      return CONFIG[profile];
    } // matched website profile
    Host.dbLog('Could not find matching URL in getProfile() call!');
  } // for each profile
}// 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, indirect_speech=false) {
 if(!indirect_speech)
   return nonQuotedRef(data); // conventional/verbatim selection
  else { 
    // pattern match the content using a vector of regexes
    data.content = transformSpeech(data.content, data.author, null, speechTransformations );
    return nonQuotedRef(data);
  }
}

function nonQuotedRef(data) { //TODO: rename 
  var template = Host.getTemplate();
  
  var substituted = template
  .replace('$CONTENT', data.content)
  .replace('$URL',data.url)
  .replace('$TITLE',data.title)  
  .replace('$AUTHOR',data.author)
  .replace('$DATE',datef(data.date))
  .replace('$ADDED',datef(data.date))
  .replace('$SCRIPT_VERSION', Host.getScriptVersion() );
  
  return substituted; 
}// 

// Output the text.
// Tries the jQuery dialog, and falls back to window.prompt()

function outputText(msg, original) {
  try {
    OUTPUT.jQueryTabbed(msg, original); 
  } 
  catch (err) {
    msg = msg.replace(/&lt;\/syntaxhighligh(.)>/g, '</syntaxhighligh$1');
    OUTPUT.msgbox(msg);
  }
}

// #############
// # Utilities #
// #############

function extract(regex) {
  return function (text) {
    return text.match(regex) [1];
  };
}
function prepend(prefix) {
  return function (text) {
    return prefix + text;
  };
}
function removeComments(html) {
  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, '{{!!}n}');
  html = html.replace(/\|\-/g, '{{!-}}');
  return html.replace(/\|/g, '{{!}}');
}// Converts HTML <a href="...">...</a> 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.*?href="http:\/\/wiki\.flightgear\.org\/File:(.*?)".*?>(.*?)<\/a>/g, '[[Media:$1|$2]]');
  // Wiki links without custom text.
  html = html.replace(/<a.*?href="http:\/\/wiki\.flightgear\.org\/(.*?)".*?>http:\/\/wiki\.flightgear\.org\/.*?<\/a>/g, '[[$1]]');
  // Links to the wiki with custom text
  html = html.replace(/<a.*?href="http:\/\/wiki\.flightgear\.org\/(.*?)".*?>(.*?)<\/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.*?href="(.*?)".*?>(.*?)<\/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 <a> links

function img2link(html) {
  html = html.replace(/<a[^<]*?href="([^<]*?)"[^<]*?><img.*?src="http:\/\/wiki\.flightgear\.org\/images\/.*?\/.*?\/(.*?)".*?><\/a>/g, '[[File:$2|250px|link=$1]]');
  html = html.replace(/<img.*?src="http:\/\/wiki\.flightgear\.org\/images\/.*?\/.*?\/(.*?)".*?>/g, '[[File:$1|250px]]');
  html = html.replace(/<a[^<]*?href="([^<]*?)"[^<]*?><img.*?src="(.*?)".*?><\/a>/g, '(see [$2 image], links to [$1 here])');
  return html.replace(/<img.*?src="(.*?)".*?>/g, '(see the [$1 linked image])');
}// Converts smilies

function forum_smilies2text(html) {
  html = html.replace(/<img src="\.\/images\/smilies\/icon_.*?\.gif" alt="(.*?)".*?>/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 style="font-weight: bold">(.*?)<\/span>/g, '\'\'\'$1\'\'\'');
  html = html.replace(/<span style="text-decoration: underline">(.*?)<\/span>/g, '<u>$1</u>');
  html = html.replace(/<span style="font-style: italic">(.*?)<\/span>/g, '\'\'$1\'\'');
  return html.replace(/<span class="posthilit">(.*?)<\/span>/g, '$1');
}// Converts code blocks

function forum_code2syntaxhighlight(html) {
  var list = html.match(/<dl class="codebox">.*?<code>(.*?)<\/code>.*?<\/dl>/g),
  data = [
  ];
  if (list === null) return html;
  for (var n = 0; n < list.length; n++) {
    data = html.match(/<dl class="codebox">.*?<code>(.*?)<\/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(/&nbsp;/g, ' ');
  if (code.match(/=?.*?\(?.*?\)?;/) !== null) lang = 'nasal';
  if (code.match(/&lt;.*?&gt;.*?&lt;\/.*?&gt;/) !== null || code.match(/&lt;!--.*?--&gt;/) !== null) lang = 'xml';
  code = code.replace(/<br\/?>/g, '\n');
  return '<syntaxhighlight lang="' + lang + '" enclose="div">\n' + code + '\n&lt;/syntaxhighlight>';
}// Converts quote blocks to Cquotes

function forum_quote2cquote(html) {
  html = html.replace(/<blockquote class="uncited"><div>(.*?)<\/div><\/blockquote>/g, '{{cquote|$1}}');
  if (html.match(/<blockquote>/g) === null) return html;
  var numQuotes = html.match(/<blockquote>/g).length;
  for (var n = 0; n < numQuotes; n++) {
    html = html.replace(/<blockquote><div><cite>(.*?) wrote.*?:<\/cite>(.*?)<\/div><\/blockquote>/, '{{cquote|$2|$1}}');
  }
  return html;
}// Converts videos to wiki style

function vid2wiki(html) {
  // YouTube
  html = html.replace(/<div class="video-wrapper">\s.*?<div class="video-container">\s*?<iframe class="youtube-player".*?width="(.*?)" height="(.*?)" src="http:\/\/www\.youtube\.com\/embed\/(.*?)".*?><\/iframe>\s*?<\/div>\s*?<\/div>/g, '{{#ev:youtube|$3|$1x$2}}');
  // Vimeo
  html = html.replace(/<iframe src="http:\/\/player\.vimeo\.com\/video\/(.*?)\?.*?" width="(.*?)" height="(.*?)".*?>.*?<\/iframe>/g, '{{#ev:vimeo|$1|$2x$3}}');
  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.

function forum_br2newline(html) {
  html = html.replace(/<br\/?><br\/?>/g, '\n');
  return html.replace(/<br\/?>/g, '\n\n');
}// Forum list to wiki style

function list2wiki(html) {
  var list = html.match(/<ul>(.*?)<\/ul>/g);
  if (list !== null) {
    for (var i = 0; i < list.length; i++) {
      html = html.replace(/<li>(.*?)<\/li>/g, '* $1\n');
    }
  }
  list = html.match(/<ol.*?>(.*?)<\/ol>/g);
  if (list !== null) {
    for (var i = 0; i < list.length; i++) {
      html = html.replace(/<li>(.*?)<\/li>/g, '# $1\n');
    }
  }
  html = html.replace(/<\/?[uo]l>/g, '');
  return html;
}
function nowiki(text) {
  return '<nowiki>' + text + '</nowiki>';
}// Returns the correct ordinal adjective

function ordAdj(date) {
  date = date.toString();
  if (date == '11' || date == '12' || date == '13') {
    return 'th';
  } else if (date.substr(1) == '1' || date == '1') {
    return 'st';
  } else if (date.substr(1) == '2' || date == '2') {
    return 'nd';
  } else if (date.substr(1) == '3' || date == '3') {
    return 'rd';
  } else {
    return 'th';
  }
}

// Formats the date to this format: Apr 26th, 2015
function datef(text) {
  var date = new Date(text);
  return MONTHS[date.getMonth()] + ' ' + date.getDate() + ordAdj(date.getDate()) + ', ' + date.getFullYear();
}
function underscore2Space(str) {
  return str.replace(/_/g, ' ');
}

// IGNORE EVERYTHING THAT FOLLOWS: 
// This is an experiment to use GA/GP (genetic programming) to help procedurally evolve xpath and regex expressions if/when the underlying websites change
// so that we don't have to manually update/edit the script accordingly (this would also work for mobile themes etc)
// For now, this is heavily based on the genetic.js framework/examples: http://subprotocol.com/system/genetic-hello-world.html
// The idea is to evolve the xpath/regex expression by evaluating its return value against the expected/desired value
// the most important thing here is having a suitable fitness function
// 



function evolve_expression_test() {
  
try {  
var genetic = Genetic.create();

// TODO: use minimizer: redundant_bytes + duration_msec + xpath.length
genetic.optimize = Genetic.Optimize.Maximize;
genetic.select1 = Genetic.Select1.Tournament2;
genetic.select2 = Genetic.Select2.Tournament2;
 
   
genetic.seed = function() {

    function randomString(len) {
        var text = "";
        var charset = "\\abcdefghijklmnopqrstuvwxyz0123456789[] ()<>*.,";
        for(var i=0;i<len;i++)
            text += charset.charAt(Math.floor(Math.random() * charset.length));
        
        return text;
    }
    
    // create random strings that are equal in length to solution
    return randomString( this.userData["solution"].length);
};
  

genetic.mutate = function(entity) {
    
    function replaceAt(str, index, character) {
        return str.substr(0, index) + character + str.substr(index+character.length);
    }
    
    // chromosomal drift
    var i = Math.floor(Math.random()*entity.length);
    return replaceAt(entity, i, String.fromCharCode(entity.charCodeAt(i) + (Math.floor(Math.random()*2) ? 1 : -1)));
};

genetic.crossover = function(mother, father) {

    // two-point crossover
    var len = mother.length;
    var ca = Math.floor(Math.random()*len);
    var cb = Math.floor(Math.random()*len);     
    if (ca > cb) {
        var tmp = cb;
        cb = ca;
        ca = tmp;
    }
        
    var son = father.substr(0,ca) + mother.substr(ca, cb-ca) + father.substr(cb);
    var daughter = mother.substr(0,ca) + father.substr(ca, cb-ca) + mother.substr(cb);
    
    return [son, daughter];
};
    
genetic.determineExcessBytes = function (text, needle) {
    return text.length - needle.length;
};
    
genetic.containsText = function (text, needle) {
    return text.search(needle);
};
  
genetic.isValid = function(exp) {

};
    
/* myFitness:
 * - must be a valid xpath/regex expression (try/call)
 * - must containsText the needle
 * - low relative offset in text (begin/end)
 * - excessBytes
 * - short expression  (expression length)
 * - expression footprint (runtime)
 */ 

// TODO: the fitness function should validate each xpath/regex first
    
    
genetic.fitness = function(entity) {
    var fitness = 0;
    var result;
    var validExp = 0.1;
    var hasToken = 0.1;
   
  
    var t = this.userData.tests[0].haystack;
    //var regex = new RegExp(this.userData.solution);
    //var output = t.match( new RegExp("From: (.*) <.*@.*>"))[1];  
    // TODO: use search & match for improving the fitness
  
    if (0)  
    try {
    var regex = new RegExp(entity);
    var output = t.search( regex);
    validExp = 10;
    }
    catch(e) {
    validExp = 2;    
    }
  
   
    
    var i;
    for (i=0;i<entity.length;++i) {
        // increase fitness for each character that matches
        if (entity[i] == this.userData["solution"][i])
            fitness += 1;
        
        // award fractions of a point as we get warmer
        fitness += (127-Math.abs(entity.charCodeAt(i) - this.userData["solution"].charCodeAt(i)))/50;
    }

 
    return fitness; // + (1*validExp + 1* hasToken);
};

genetic.generation = function(pop, generation, stats) {
    // stop running once we've reached the solution
    return pop[0].entity != this.userData["solution"];
};

genetic.notification = function(pop, generation, stats, isFinished) {

    function lerp(a, b, p) {
        return a + (b-a)*p;
    }
    
    var value = pop[0].entity;
    this.last = this.last||value;
    
    if (pop != 0 && value == this.last)
        return;
    
    
    var solution = [];
    var i;
    for (i=0;i<value.length;++i) {
        var diff = value.charCodeAt(i) - this.last.charCodeAt(i);
        var style = "background: transparent;";
        if (diff > 0) {
            style = "background: rgb(0,200,50); color: #fff;";
        } else if (diff < 0) {
            style = "background: rgb(0,100,50); color: #fff;";
        }

        solution.push("<span style=\"" + style + "\">" + value[i] + "</span>");
    }
  
    var t = this.userData.tests[0].haystack;
    //console.log("haystack is:"+t);
    // "From: John Doe <John@do...> - 2020-07-02 17:36:03", needle: "John Doe"}, /From: (.*) <.*@.*>/
    var regex = new RegExp(this.userData.solution);
    //var output = t.match( new RegExp("From: (.*) <.*@.*>"))[1];  
    // TODO: use search & match for improving the fitness
    var output = t.search( new RegExp(value));
    
    
    var buf = "";
    buf += "<tr>";
    buf += "<td>" + generation + "</td>";
    buf += "<td>" + pop[0].fitness.toPrecision(5) + "</td>";
    buf += "<td>" + solution.join("") + "</td>";
    buf += "<td>" + output + "</td>";
    buf += "</tr>";
    $("#results tbody").prepend(buf);
    
    this.last = value;
};
  
  
  /*
genetic.notification2 = function(pop, generation, stats, isFinished) {

    function lerp(a, b, p) {
        return a + (b-a)*p;
    }
    
    var value = pop[0].entity;
    this.last = this.last||value;
    
    if (pop != 0 && value == this.last)
        return;

    
    var solution = [];
    var i;
    for (i=0;i<value.length;++i) {
    
    solution.push(value[i]);
 } 
    console.log("Generation:"+ generation + " Fitness:" + pop[0].fitness.toPrecision(5) + " Solution:" + solution.join(""));
  
    this.last = value;
};
  */
    
      
var config = {
            "iterations": 4000
            , "size": 250
            , "crossover": 0.3
            , "mutation": 0.4
            , "skip": 30 // notifications
            //, "webWorkers": false
        };


/*
var profile = CONFIG['Sourceforge Mailing list'];
var posting = profile.tests[0];
var author_xpath = profile.title.xpath;
*/

var regexTests = [
  {haystack: "From: John Doe <John@do...> - 2020-07-02 17:36:03", needle: "John Doe"}, 
  {haystack: "From: Marc Twain <Marc@ta...> - 2010-01-03 07:36:03", needle: "Marc Twain"},
  {haystack: "From: George W. Bush <GWB@wh...> - 2055-11-11 17:33:13", needle: "George W. Bush"}
];
  
// the regex we want to evolve
var solution = "From: (.*) <.*@.*>";

// let's assume, we'd like to evolve a regex expression like this one
var userData = {
            solution: solution,
            tests: regexTests                         
};    
    
genetic.evolve(config, userData);

    
//console.log("genetic.js is loaded and working, but disabled for now");    
    
  
} // try
catch (e) {
  console.log("genetic.js error:\n" +e.message);
} // catch
  
} // evolveExpression_test()


if(0) //TODO: expose via development tab
try {
  // https://github.com/cazala/synaptic
  var Neuron = synaptic.Neuron,
    Layer = synaptic.Layer,
    Network = synaptic.Network,
    Trainer = synaptic.Trainer,
    Architect = synaptic.Architect;
  
  function Perceptron(input, hidden, output)
{
    // create the layers
    var inputLayer = new Layer(input);
    var hiddenLayer = new Layer(hidden);
    var outputLayer = new Layer(output);

    // connect the layers
    inputLayer.project(hiddenLayer);
    hiddenLayer.project(outputLayer);

    // set the layers
    this.set({
        input: inputLayer,
        hidden: [hiddenLayer],
        output: outputLayer
    });
}

// extend the prototype chain
Perceptron.prototype = new Network();
Perceptron.prototype.constructor = Perceptron;
  
var myPerceptron = new Perceptron(2,3,1);
var myTrainer = new Trainer(myPerceptron);

myTrainer.XOR(); // { error: 0.004998819355993572, iterations: 21871, time: 356 }

myPerceptron.activate([0,0]); // 0.0268581547421616
myPerceptron.activate([1,0]); // 0.9829673642853368
myPerceptron.activate([0,1]); // 0.9831714267395621
myPerceptron.activate([1,1]); // 0.02128894618097928
  
   
console.log("Syntaptic loaded");
} catch(e) {
  UI.alert(e.message);
}