Greasy Fork

Greasy Fork is available in English.

Instant-Cquotes

Automatically converts FlightGear mailing list and forum quotes into MediaWiki markup (cquotes).

当前为 2016-05-05 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Instant-Cquotes
// @version     0.32
// @description Automatically converts FlightGear mailing list and forum quotes into MediaWiki markup (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/6/62/FlightGear_logo.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
// @grant       GM_registerMenuCommand
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_addStyle
// @grant       GM_getResourceText
// @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.
//
// 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'));
  // http://wiki.greasespot.net/GM_info
  var scriptVersion = ' (v' + GM_info.script.version + ')';
  //console.log(GM_info);
} 
catch (error) {
console.log('Could not add style or determine script version');
}

'use strict';

// prevent conflicts with jQuery used on webpages: https://wiki.greasespot.net/Third-Party_Libraries#jQuery
this.$ = this.jQuery = jQuery.noConflict(true);

// https://wiki.greasespot.net/GM_registerMenuCommand
// https://wiki.greasespot.net/Greasemonkey_Manual:Monkey_Menu#The_Menu
var commands = [
  {name:'Setup quotes',callback:setupDialog, hook:'S' },
  {name:'Check quotes',callback:selfCheckDialog, hook:'C' },
];
for (var i in commands ) {
GM_registerMenuCommand(commands[i].name, commands[i].callback, commands[i].hook);
}

// 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() {return true;}  },
     {name:'make_doc', test: function() {return true;}   },
     {name:'eval_xpath', test: function() {return true;} },
     {name:'JSON de/serialization', test: function() {
       //console.log("running json test");
       var identifier = 'unit_tests.json_serialization';
       var hash1 = {x:1,y:2,z:3};
       Host.set_persistent(identifier, hash1, true);
       var hash2 = Host.get_persistent(identifier,null,true);
       
       return JSON.stringify(hash1) === JSON.stringify(hash2);
     } // callback 
     }
    
  ], // end of APITests
  
  runAPITests: function(host, callback) {
    for(var t in 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);
    } // foreach test
  }, // runAPITests
  
  ///////////////////////////////////////
  // supported  script engines:
  ///////////////////////////////////////
  
  GreaseMonkey: {
    
  dbLog: function (message) {
  if (Boolean(DEBUG)) {
    console.log('Instant cquotes:' + message);
  }
  }, // dbLog()

    
   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()

  
    // 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
    
  } // end of GreaseMonkey environment, add other environments below
  
}; // Environment hash - intended to help encapsulate host specific stuff (APIs)

// the Host hash contains all platform/browser-specific APIs
var Host = Environment.getHost();
/*
Environment.runAPITests(Host, function(meta) {
  console.log('Running API test '+meta.name);
});
*/

// move DEBUG handling to a persistent configuration flag so that we can configure this using a jQuery dialog (defaulted to false)
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

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',
    event: 'document.onmouseup', // when to invoke the event handler
    event_handler: function () {
      Host.dbLog('FlightGear wiki handler active');
      // this is where the logic for a wiki mode can be added over time (for now, it's a NOP)
    }, // 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': {
    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: {
      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': {
    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)
    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')
      ]
    }
  }
};
// this being a greasemonkey user-script, we are not 
// subject to usual browser restrictions
function setClipboard(msg) {
  // http://wiki.greasespot.net/GM_setClipboard
  GM_setClipboard(msg);
}// 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' + scriptVersion, msg);
    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 = '<form name=\'target\'>Target: <select name=\'selection\' onchange=\'EventHandlers.updateTarget();\'><option value=\'0\'>to wiki</option><option value=\'1\'>to forum</option></select>Format: <select name=\'format\' onchange=\'EventHandlers.updateFormat();\'><option value=\'0\'>refonly</option><option value=\'1\'>fgcquote</option></select></form>';
    var diagDiv = $('<div id="MyDialog"><textarea id="quotedtext" rows="10"cols="80" style="width: 290px; height: 290px">' + msg + '</textarea>' + target_format + '</div>');
    var diagParam = {
      title: 'Copy your quote with Ctrl+c' + scriptVersion,
      modal: true,
      width: 'auto',
      buttons: [
        /*
        {
          text: 'Check',
          click: selfCheckDialog
        },
        */
        /*
        {
          text: 'Setup',
          click: setupDialog
        },
        */
        {
          text: 'Select all',
          click: function () {
            setClipboard(msg);
            $('#quotedtext').select();
          }
        },
        {
          text: 'OK',
          click: function () {
            setClipboard(msg);
            $(this).dialog('close');
          }
        }
      ]
    };
    diagDiv.dialog(diagParam);
  }
};

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

var speechTransform_1st_to_3rd = [
// ordering is crucial here (most specific first, least specific/most generic last)
  {'I myself': '$author himself'},
  {'I am': ' $author is'},
  {'I can': '$author can'},
  {'I have': '$author has'},
  {'I should': '$author should'},
  {'I shall': '$author shall'},
  {'I may': '$author may'},
  {'I will': '$author will'},
  {'I would': '$author would'},
  {'by myself': 'by $author'},
  {'and I': 'and $author'},
  {'and me': 'and $author'},
  {'and myself': 'and $author'},
  
  // least specific stuff last (broad/generic stuff is kept as is, with author clarification added in parentheses)
  {'I': 'I ($author)'},
  {'me': 'me ($author)'},
  {'my': 'my ($author)'},
  {'myself': 'myself ($author)'},
  {'mine': '$author'}
  
];

// try to assist in transforming speech using the transformation vector passed in
// still needs to be exposed via the UI
function transformSpeech(text, author, gender, transformations) {
  // TODO: foreach transformation in vector, replace the search pattern with the matched string (replacing author/gender as applicable)
  return text;
} // transformSpeech

// run a self-test
(function() {
var author ="John Doe";
var transformed = transformSpeech("I have decided to commit a new feature", author, null, speechTransform_1st_to_3rd );
if (transformed !== "John Doe has decided to commit a new feature")
  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
function init() {
  Host.dbLog('page load handler invoked');
  var profile = getProfile();
  
  Host.dbLog("Profile type is:"+profile.type);
  
  
  if (profile.type=='wiki') {
    profile.event_handler(); // just for testing
    return;
  }
  
  if (Boolean(USE_NG)) {
    Host.dbLog('using devel version (WIP)');
    document.onmouseup = instantCquoteNG;
  } else {
    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");
    
  }
}



// an experimental version (non-functional for now)
function instantCquoteNG() {
  Host.dbLog('experimental code triggered, running AJAX test');
  //self-test
  Host.download('http://sourceforge.net/p/flightgear/mailman/message/27369425/', function (response) {
    console.log('ajax request status:' + response.statusText);
  });
} // instantCquoteNG


// The main function
function instantCquote() {
  var profile = getProfile();
  
  var selection = document.getSelection(),
  output = {
  },
  field = {
  };
  try {
    var 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("Extracing 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 (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
    Host.dbLog('Testing profile:' + profile);
    for (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 = '<h3>Important APIs:</h3><div id="api_checks">(to be added here using the Environment.runAPITests() helper)</div>';
  Environment.runAPITests(Host, function(meta) {
  //sections.$('#api_checks').append(meta.name+'<p/>');
  console.log('Running API test '+meta.name);
  console.log((meta.test())?'success':'fail');
  
});

  runProfileTests();
  
  for (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
    var test_results = '';
    for (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>');
  //$('#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 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() {
  for (var profile in CONFIG) {
    if (window.location.href.match(CONFIG[profile].url_reg) !== null) {
      Host.dbLog('Matching website profile found');
      var invocations = Host.get_persistent(GM_info.script.version, 0);
      Host.dbLog('Number of script invocations for version ' + GM_info.script.version + ' 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 ' + GM_info.script.version + '\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(GM_info.script.version, 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 '<ref>' + createCiteWeb(data) + '</ref>';
}
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 setClipboard() here
  } 
  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

function escapePipes(html) {
  html = html.replace(/\|\|/g, '{{!!}}');
  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

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, ' ');
}