// ==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 // @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'; // 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; function set_persistent(key,value) { // https://wiki.greasespot.net/GM_setValue GM_setValue(key,value); } //make_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 = "
Target: Format:
"; var diagDiv = $('
' + target_format + '
'); 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 function setupDialog() { alert("configuration dialog is not yet implemented"); } // 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 ''+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 { METHODS.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(/(.*?)<\/a>/g, '[[Media:$1|$2]]'); // Wiki links without custom text. html = html.replace(/http:\/\/wiki\.flightgear\.org\/.*?<\/a>/g, '[[$1]]'); // Links to the wiki with custom text html = html.replace(/(.*?)<\/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>/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 links function img2link(html) { html = html.replace(/<\/a>/g, '[[File:$2|250px|link=$1]]'); html = html.replace(//g, '[[File:$1|250px]]'); html = html.replace(/<\/a>/g, '(see [$2 image], links to [$1 here])'); return html.replace(//g, '(see the [$1 linked image])'); } // Converts smilies function forum_smilies2text(html) { html = html.replace(/(.*?)/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>/g, '\'\'\'$1\'\'\''); html = html.replace(/(.*?)<\/span>/g, '$1'); html = html.replace(/(.*?)<\/span>/g, '\'\'$1\'\''); return html.replace(/(.*?)<\/span>/g, '$1'); } // Converts code blocks function forum_code2syntaxhighlight(html) { var list = html.match(/
.*?(.*?)<\/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*?