// ==UserScript== // @name Instant-Cquotes // @version 0.31 // @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 // @downloadURL none // ==/UserScript== // // This work has been released into the public domain by their authors. This // applies worldwide. // In some countries this may not be legally possible; if so: // The authors grant anyone the right to use this work for any purpose, without // any conditions, unless such conditions are required by law. // // 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'; // 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 GM_registerMenuCommand('Check cquotes', selfCheckDialog, 'C'); // set this to true continue working on the new mode supporting // asynchronous content fetching via AJAX var USE_NG = false; var Environment = { getHost: function() { return Environment.GreaseMonkey; // return the only/default host }, GreaseMonkey: { set_persistent: function(key,value) { // https://wiki.greasespot.net/GM_setValue GM_setValue(key, value); }, // set_persistent get_persistent: function(key,default_value) { // https://wiki.greasespot.net/GM_getValue return GM_getValue(key, default_value); } // get_persistent } // end of GreaseMonkey environment, add other environments below }; // Environment hash - intended to help encapsulate host specific stuff (APIs) var Host = Environment.getHost(); // 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); dbLog("Debug mode is:"+DEBUG); function DEBUG_mode() { // reset script invocation counter for testing purposes dbLog('Resetting script invocation counter'); Host.set_persistent(GM_info.script.version, 0); } if (DEBUG) DEBUG_mode(); // 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 () { 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 '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 = '
'; var diagDiv = $('' + test_results + '
' + message + status + '
').css('color', color); setupDiv.append(statusDiv); }); setupDiv.dialog(); } // setupDialog function getProfile() { for (var profile in CONFIG) { if (window.location.href.match(CONFIG[profile].url_reg) !== null) { dbLog('Matching website profile found'); var invocations = Host.get_persistent(GM_info.script.version, 0); 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) { 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) dbLog("increment number of script invocations"); Host.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 // (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) { dbLog("Trying to get PostId with defined focus"); selection = selection.focusNode.parentNode; } else { dbLog("Trying to get PostId with undefined focus"); selection = selection.anchorNode.parentNode; } while (selection.id.match(profile.content.idStyle) === null) { selection = selection.parentNode; } 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') { dbLog("Returning content (selection)"); return profile[field].selection(); } else { dbLog("Extracting field via xpath:"+field); 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 '' + createCiteWeb(data) + ''; } function createCiteWeb(data) { var date_added = new Date(); var wikiText = '{{cite web\n' + ' |url = ' + data.url + '\n' + ' |title = ' + nowiki(data.title) + '\n' + ' |author = ' + nowiki(data.author) + '\n' + ' |date = ' + datef(data.date) + '\n' + ' |added = ' + datef(date_added.toDateString()) + '\n' + ' |script_version = ' + GM_info.script.version + '\n' + ' }}\n'; return wikiText; }// Output the text. // Tries the jQuery dialog, and falls back to window.prompt() function outputText(msg) { try { OUTPUT.jQueryDiag(msg); // TODO: unify code & call setClipboard() here } catch (err) { msg = msg.replace(/<\/syntaxhighligh(.)>/g, '/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 ... 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(/(.*?)<\/code>.*?<\/dl>/g),
data = [
];
if (list === null) return html;
for (var n = 0; n < list.length; n++) {
data = html.match(/.*?(.*?)<\/code>.*?<\/dl>/);
html = html.replace(data[0], processCode(data));
}
return html;
}// Strips any whitespace from the beginning and end of a string
function stripWhitespace(html) {
html = html.replace(/^\s*?(\S)/, '$1')
return html.replace(/(\S)\s*?\z/, '$1');
}// Process code, including basic detection of language
function processCode(data) {
var lang = '',
code = data[1];
code = code.replace(/ /g, ' ');
if (code.match(/=?.*?\(?.*?\)?;/) !== null) lang = 'nasal';
if (code.match(/<.*?>.*?<\/.*?>/) !== null || code.match(/<!--.*?-->/) !== null) lang = 'xml';
code = code.replace(/
/g, '\n');
return '\n' + code + '\n</syntaxhighlight>';
}// Converts quote blocks to Cquotes
function forum_quote2cquote(html) {
html = html.replace(/(.*?)<\/div><\/blockquote>/g, '{{cquote|$1}}');
if (html.match(//g) === null) return html;
var numQuotes = html.match(//g).length;
for (var n = 0; n < numQuotes; n++) {
html = html.replace(/(.*?) wrote.*?:<\/cite>(.*?)<\/div><\/blockquote>/, '{{cquote|$2|$1}}');
}
return html;
}// Converts videos to wiki style
function vid2wiki(html) {
// YouTube
html = html.replace(/\s.*?\s*?