FlightGear wiki:Instant-Refs

From FlightGear wiki
Revision as of 23:20, 2 May 2016 by Elgaton (talk | contribs) (Reorganized wiki page (put the installation/usage sections at the top and the development-related ones at the bottom to aid new contributors))
Jump to navigation Jump to search
Instant-Cquotes script in Firefox
Instant-Cquotes screenshot prototyping runtime format selection

The Instant-Cquotes script is a browser addon (user script) implemented in JavaScript in order to convert excerpts from FlightGear forum or mailing list postings into MediaWiki markup/quotes. It is supported by Firefox, Google Chrome/Chromium, Opera and Safari. It is being developed and maintained by a group of volunteers involved in maintaining the wiki and in trying to provide more up-to-date information to end-users who may not be as involved in the various FlightGear-related communication channels.

Background and motivation

FlightGear's development is, at best, "self-coordinated", meaning that contributors discuss ideas and make proposals to contribute in a certain fashion and then team up to implement certain features and building blocks.

Unfortunately, due to a lack of development manpower, many ideas are not implemented immediately; it is, thus, important to know their pros and cons, as well as who originally proposed them and/or might help with their implementation, even after a long time, preferably with links to the original discussions so that new contributors can decide whether to get involved in some effort or not. The project documentation, however, is usually significantly lacking behind:[1] many core developers update it seldomly, if not anymore, as it takes time to write it, as well as an understanding of the inner workings of a complex system such as FlightGear.[2] Forum and mailing lists discussions have therefore become the only up-to-date (albeit difficult to filter) source of information about recent development progress.

The aim of the Instant-Cquotes script is to help wiki editors to copy relevant excerpts from such sources, formatting them as proper quotations, and bootstrap new articles collecting them until a dedicated rewrite is made. It can also be used to reuse announcements to update the changelogs, newsletter or the Release plan/Lessons learned page.

Installation

  1. Install a user script manager. On Firefox, you can use Greasemonkey; on Chrome/Chromium, Opera or Safari, you can use Tampermonkey (download links: Chrome/Chromium, Opera, Safari).
  2. Visit the Instant-Cquotes page on GreasyFork and click on Install this script (green button). If Greasemonkey/Tampermonkey prompts you to confirm the installation, agree to do so.
click the green button to install the script

Manual installation

Note  If you use this method to install the script, GreaseMonkey/TamperMonkey will not update it automatically whenever a new version is released.
  • Firefox
  1. Install Greasemonkey.
  2. Save the script below as instant_cquotes.user.js, then drag-and-drop it into Firefox.
Screenshot showing the Greasemonkey setup dialog (on Firefox)
  • Chrome/Chromium, Opera, or Safari
  1. Install Tampermonkey.
  2. Navigate to Add a new Script.
  3. Copy and paste the script below into the editing window.
  4. Click the Save button (just above the Search button).

Mobile installation

As of May 2016, there is no separate version available for mobile use. Your best chance is installing a userscript addon on Android, like one of those:

For installation instructions, refer to Tampermonkey for Android or How To Access Greasemonkey Scripts on Android Phones.

Testing/feedback would obviously be appreciated - if in doubt, feel free to just edit the wiki page to add your findings/questions.

Configuration

The configuration dialog for the Instant-Cquotes script

As of version 0.30, a dedicated configuration dialog is in the process of being added, so that certain script features can be dynamically configured, without having to edit the script. For now, this is merely a placeholder that provides a checkbox to easily enable/disable the debug mode. In the future, we are hoping to also expose other features this way.

Usage

Instant-Cquotes script, with updates contributed by Red Leader
Screenshot showing Instant-Cquotes 0.30 at work
  1. Go to some mailing list archive URL, for example [1] or any forum message, such as

Template:Forumref[2].

  1. Select the relevant portion of text.
  2. When you release the mouse button, a box will appear containing the converted text (for now, mainly properly-referenced quotes for the wiki).
  3. As the text will already be selected for you, press Ctrl+C to copy it (no longer necessary).
  4. Paste the text into the desired wiki page.

For example, selecting part of the forum post in the link above is rendered as follows:

Cquote1.png The upcoming FlightGear version (3.2) will contain a canvas-based map dialog, including a modular "plugin" system for creating custom map layers and charts with roughly ~50 lines of code, most of it boilerplate.

This is entirely XML/Nasal based (scripted) - symbols can be pretty much anything, raster or vector images (png or svg), but even animated. Styling can be customied, too. For more info, I suggest to check out: MapStructure#Porting the map dialog

MapStructureDialog.png
— Hooray (Jun 14th, 2014). Re: Get objects to show up on Map/Radar.
(powered by Instant-Cquotes)
Cquote2.png

On quoting

1rightarrow.png See FlightGear wiki:Quoting Guidelines for the main article about this subject.

Using the Instant-Cquotes script is a good way to bootstrap and write some preliminary notes; however, while quotes might be useful to understand how undocumented subsystems and features work and are definitely better than nothing, they are not meant to replace proper, structured and well-written wiki articles.[3]

One way to convert pages bootstrapped using quotes is to extract relevant information from them and keep citations only as references; in case important details are missing, they can be asked for on the mailing lists (on the forum, the chance to get a complete answer might be lower).[4] Another option might be moving the quotes to the Talk page for each entry, which would preserve the sources without clogging up the articles.[5]

As a matter of fact, the whole paragraph above was assembled using this approach; to see for yourself, look up the references at the end of this page.

Development

Note  A Chrome/Chromium-specific extension that will not need Tampermonkey installed is under development.
Instant-Cquotes debug mode

Mobile edition

Note  As of 02/2016, Hooray is contemplating to make this available as an addon for Android phones.

Issues/limitations

Bugs

  • It's eating characters, apparently related to regex/xpath handling - e.g. words like "analyzing" are turned into "analying" [3]

Non working URLs

Feature requests & ideas

  • encode script settings in created markup, for future processing/updating of quotes
  • look up [x] references and replace with the corresponding link [4]
  • support named refs for combining identical refs [5]
  • adopt Template:Forumref
  • implement a less obnoxious quoting mode, without quotes, where only the ref part would be added, e.g. see the example at Graphics Card Profiles (it's still 99% quotes, but much less annoying) Done Done
  • attachment support: identify attachments and link to them: https://sourceforge.net/p/flightgear/mailman/message/11683451/
  • bulletin points: if there is a colon (:) followed by at least two dashes (-), split up everything after the colon to turn each dash into an asterisk (wiki markup for bulletin points), followed by a newline [6]
  • generic URL/template matching, e.g. for for sourceforge commit IDs
  • make filters/conversions configurable via checkboxes (nowiki, wrap in alert/note boxes)
  • make syntax highlighting configurable (language, mode) ?
  • consider using something like the Roles template to turn contributor names into tooltips where contributor roles are shown (core dev, fgdata committer etc)
  • introduce support for tag clouds to help categorize/classify related quotes
  • consider making transformations optional/configurable using check boxes in the jQuery dialog
  • add new input method, for quotes that need to be updated/converted (added script version specifically for this purpose), should also add extraction/processing date
  • investigate why not all mailing list archives/postings are supported correctly: http://sourceforge.net/p/flightgear/mailman/message/8090479/ (problem traced to getPostID())
  • explore having a ref-only mode without using cquotes, i.e. just copy/paste quotes with proper ref tags and a references section, to rewrite the whole thing (possibly with templates for different purposes, e.g. newsletter/changelog)
  • should use Template:Forumref (category:link templates)
  • should be updated to use the new repo/flightgear file templates created by Johan & RedLeader Not done Not done
  • resolve links to forum threads to look up the title for the topic/posting, so that the posting's title can be used for those links, instead of just the URL - we can probably do that by making an AJAX call to open URLs asynchronously and extract the title for the thread/posting to come up with something like A call to developers-Lockheed -L188 Electra ([7]) 50}% completed
  • convert quoted bug tracker URLs to use the issue template on the wiki Not done Not done
  • do regex/xpath validation, and display any errors (e.g. template/theme changes on the forum would currently break the script) Not done Not done
  • increased focus on supporting different output formats, maybe using a simple jQuery based wizard (wiki, forum, newsletter, changelog) Not done Not done
  • token matching for keywords/acronyms to link to the corresponding wiki articles (e.g. Nasal, Canvas, FG_ROOT, FG_HOME etc) Not done Not done
  • Add support for tickets, merge requests comments and FGUK forum. (also see FlightGear wiki talk:Instant-Cquotes#more sources)
  • GET-encoded SID arguments should be stripped from forum URLs. Not done Not done
  • Links to repositories should be converted to use wiki templates. Not done Not done
  • The regexes used may fail if the HTML DOM of the source changes (e.g., phpBB/theme update)
    • Show a warning when that's the case. Not done Not done
    • Try multiple regexes in order. Not done Not done
  • Use the script to update previously created Cquotes automatically
    • Instead of using the getSelection() helper, we could register a match for wiki.flightgear.org with action=edit set, so that we can directly process all text of an edited page, using AJAX calls to open the URL in the background. Not done Not done
    • See MW:API:Edit § Editing via Ajax

Changelog

Note  Contributors are invited to document their changes here, please also add your wiki handle so that others can more easily get in touch.
  • support for persistent settings and a jQuery setup dialog with persistence
  • hosting is moved, to allow auto-updates [8] [9] [10]
  • changed to ref-only quotes for now, not using the FGCquote template anymore, due to its obnoxious appearance on quote-heavy pages (should probably become a runtime option instead)
  • updated to use https for forum postings
  • add version info to each created quote, i.e. for future updates
  • add helper for opening websites asynchronously using AJAX (also via GM helper API)
  • display version number in output dialog
  • begin using the GreaseMonkey API for setting clipboard content

The script

Public domain This work has been released into the public domain by its author, FlightGear contributors. This applies worldwide.
In some countries this may not be legally possible; if so:
FlightGear contributors grants anyone the right to use this work for any purpose, without any conditions, unless such conditions are required by law.
Note  Anybody interested in contributing to the code is invited to directly edit this wiki article. From 05/2016, the script is hosted on GreasyFork to allow automatic updates. If you'd like to see your changes applied, please bump the version number and Elgaton will upload it in the state it was when the version number was bumped. Make sure to perform thorough testing before the bump to prevent unexpected breakage. Thank you!
// ==UserScript==
// @name        Instant-Cquotes
// @version     0.30
// @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/*
// @namespace   http://wiki.flightgear.org/FlightGear_wiki:Instant-Cquotes
// @run-at      document-end
// @require     https://code.jquery.com/jquery-2.1.4.min.js
// @require     https://code.jquery.com/ui/1.11.4/jquery-ui.min.js
// @resource    jQUI_CSS https://code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css
// @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) {
}
'use strict';


// move DEBUG handling to a persistent configuration flag so that we can configure this using a jQuery dialog (defaulted to false)
var DEBUG = get_persistent("debug_mode_enabled",false);

function DEBUG_mode() {
  // reset script invocation counter for testing purposes
  dbLog("Resetting script invocation counter");
  set_persistent(GM_info.script.version,0);
}

if (DEBUG) 
  DEBUG_mode();

// set this to true continue working on the new mode supporting
// asynchronous content fetching via AJAX
var USE_NG=false;

function set_persistent(key,value) {
  // https://wiki.greasespot.net/GM_setValue
  GM_setValue(key,value);
} //set_persistent

function get_persistent(key, default_value) {
// https://wiki.greasespot.net/GM_getValue
  return GM_getValue(key, default_value);
} // get_persistent

// hash with supported websites/URLs

var CONFIG = {
  'Mailing list': {
    url_reg: "^(http|https):\/\/sourceforge\.net\/p\/flightgear\/mailman\/.*/",
    content: {
      selection: getSelectedText,
      idStyle: /msg[0-9]{8}/,
      parentTag: [
        'tagName',
        'PRE'
      ]
    },
// 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')
    }
  },
// next website/URL (forum)
  'FlightGear forum': {
    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
      ]
    },
    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 METHODS = {
  // 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: '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 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);
dbLog('Instant Cquotes: page load handler registered');
// Initialize
function init() {
  dbLog('page load handler');
   if (Boolean(USE_NG)) {
    dbLog("devel version (WIP)");
    document.onmouseup = instantCquoteNG;
   }else {
     dbLog("standard version");
     document.onmouseup = instantCquote;
   }
}

function OpenLink(url, callback){
  // http://wiki.greasespot.net/GM_xmlhttpRequest
  GM_xmlhttpRequest({
  method: "GET",
  url: url,
  onload: callback
});
}

// an experimental version (non-functional for now)
function instantCquoteNG() {
  dbLog("experimental NN callback triggered");
  
  //self-test
  OpenLink("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(),
  selection = document.getSelection(),
  output = {
  },
  field = {
  };
    
  try {
    var post_id = getPostId(selection, profile);
  } 
  catch (error) {
    dbLog("Failed extracting post id\nProfile:"+profile)
    return;
  }
  if (selection.toString() === '') {
    dbLog('No text is selected, aborting function');
    return;
  }
  if (!checkValid(selection, profile)) {
    dbLog('Selection is not valid, aborting function');
    return;
  }
  for (field in profile) {
    if (field === 'name') continue;
    var fieldData = extractFieldInfo(profile, post_id, field);
    var transform = profile[field].transform;
    if (transform !== undefined) {
      dbLog('Field \'' + field + '\' before transformation:\n\'' + fieldData + '\'');
      fieldData = applyTransformations(fieldData, transform);
      dbLog('Field \'' + field + '\' after transformation:\n\'' + fieldData + '\'');
    }
    output[field] = fieldData;
  }
  output.content = stripWhitespace(output.content);
  output = createCquote(output);
  outputText(output);
}
// Gets to correct profile


// show a simple configuration dialog (WIP)
function setupDialog() {
  //alert("configuration dialog is not yet implemented");
  var checked = (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">configuration dialog is not yet implemented</p><label><input id="debugcb" type="checkbox"'+checked+'>Enable Debug mode</label></div>');
 
  setupDiv.click(function() {
    dbLog("changing persistent debug state");
    set_persistent("debug_mode_enabled", $("#debugcb").is(':checked'));
});
                          
  setupDiv.dialog();
} // setupDialog

function getProfile() {
  for (var profile in CONFIG) {
    if (window.location.href.match(CONFIG[profile].url_reg) !== null) {
      dbLog("Matching profile found");
      
      var invocations=get_persistent(GM_info.script.version,0);
      dbLog("Number of script invocations for version "+GM_info.script.version+" is:"+invocations);
      if (invocations==0) {
        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 version number as the key, to prevent the config dialog from showing up again
      set_persistent(GM_info.script.version, invocations+1)
      return CONFIG[profile];
    }
    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;
  }
  dbLog('instantCquote(): Unprocessed HTML\n\'' + html + '\'');
  return html;
}
// Gets the selected text

function getSelectedText() {
  return document.getSelection().toString();
}
// Get the ID of the post

function getPostId(selection, profile, focus) {
  if (focus !== undefined) {
    selection = selection.focusNode.parentNode;
  } else {
    selection = selection.anchorNode.parentNode;
  }
  while (selection.id.match(profile.content.idStyle) === null) {
    selection = selection.parentNode;
  }
  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') {
    return profile[field].selection();
  } else {
    var xpath = '//*[@id="' + id + '"]/' + profile[field].xpath;
    return 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);
      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 {
    METHODS.jQueryDiag(msg);
    // TODO: unify code & call setClipboard() here
  } 
  catch (err) {
    msg = msg.replace(/&lt;\/syntaxhighligh(.)>/g, '</syntaxhighligh$1');
    METHODS.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/OpenLink 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, ' ');
}
function dbLog(message) {
  if (Boolean(DEBUG)) {
    console.log("Instant cquotes:"+message);
  }
}
References
  1. Curtis Olson (Jul 27th, 2011). Re: [Flightgear-devel] The state of things in Flight Gear.
  2. Hal V. Engel (Jul 27th, 2011). Re: [Flightgear-devel] The state of things in Flight Gear.
  3. bugman (Apr 17th, 2016). Re: What is the QT launcher?.
  4. James Turner (Mar 21st, 2016). Re: [Flightgear-devel] Wiki Quotes.
  5. Stuart Buchanan (Mar 19th, 2016). Re: [Flightgear-devel] Wiki Quotes.