Greasy Fork

Instant-Cquotes

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

目前为 2016-05-02 提交的版本。查看 最新版本

// ==UserScript==
// @name        Instant-Cquotes
// @version     0.28
// @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, Elgaton & Red Leader (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_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';
// Define constants
var DEBUG = false;

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


// 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('http://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('http://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: '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('mouseup event');
   if (Boolean(USE_NG)) {
    dbLog("Instant Cquotes:devel version (WIP)");
    document.onmouseup = instantCquoteNG;
   }else {
     dbLog("Instant Cquotes: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('instantCquote(): No text is selected, aborting function');
    return;
  }
  if (!checkValid(selection, profile)) {
    dbLog('instantCquote(): 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('instantCquote(): Field \'' + field + '\' before transformation:\n\'' + fieldData + '\'');
      fieldData = applyTransformations(fieldData, transform);
      dbLog('instantCquote(): Field \'' + field + '\' after transformation:\n\'' + fieldData + '\'');
    }
    output[field] = fieldData;
  }
  output.content = stripWhitespace(output.content);
  output = createCquote(output);
  outputText(output);
}
// Gets to correct profile

function getProfile() {
  for (var profile in CONFIG) {
    if (window.location.href.match(CONFIG[profile].url_reg) !== null) {
      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(message);
  }
}