Instant-Cquotes

Converte automaticamente citazioni dalla mailing list e dal forum di FlightGear in marcatori MediaWiki (cquote).

  1. // ==UserScript==
  2. // @name Instant-Cquotes
  3. // @name:it Instant-Cquotes
  4. // @license public domain
  5. // @version 0.39
  6. // @date 2016-05-20
  7. // @description Automatically converts selected FlightGear mailing list and forum quotes into post-processed MediaWiki markup (i.e. cquotes).
  8. // @description:it Converte automaticamente citazioni dalla mailing list e dal forum di FlightGear in marcatori MediaWiki (cquote).
  9. // @author Hooray, bigstones, Philosopher, Red Leader & Elgaton (2013-2016)
  10. // @supportURL http://wiki.flightgear.org/FlightGear_wiki:Instant-Cquotes
  11. // @icon http://wiki.flightgear.org/images/2/25/Quotes-logo-200x200.png
  12. // @match https://sourceforge.net/p/flightgear/mailman/*
  13. // @match http://sourceforge.net/p/flightgear/mailman/*
  14. // @match https://forum.flightgear.org/*
  15. // @match http://wiki.flightgear.org/*
  16. // @namespace http://wiki.flightgear.org/FlightGear_wiki:Instant-Cquotes
  17. // @run-at document-start
  18. // @require https://code.jquery.com/jquery-1.10.2.js
  19. // @require https://code.jquery.com/ui/1.11.4/jquery-ui.js
  20. // @require https://cdn.jsdelivr.net/genetic.js/0.1.14/genetic.js
  21. // @require https://cdn.jsdelivr.net/synaptic/1.0.4/synaptic.min.js
  22. // @resource jQUI_CSS https://code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css
  23. // @resource myLogo http://wiki.flightgear.org/images/2/25/Quotes-logo-200x200.png
  24. // @grant GM_registerMenuCommand
  25. // @grant GM_setValue
  26. // @grant GM_getValue
  27. // @grant GM_addStyle
  28. // @grant GM_getResourceText
  29. // @grant GM_getResourceURL
  30. // @grant GM_setClipboard
  31. // @grant GM_xmlhttpRequest
  32. // @noframes
  33. // ==/UserScript==
  34. //
  35. // This work has been released into the public domain by their authors. This
  36. // applies worldwide.
  37. // In some countries this may not be legally possible; if so:
  38. // The authors grant anyone the right to use this work for any purpose, without
  39. // any conditions, unless such conditions are required by law.
  40. //
  41. // This script has a number of dependencies that are implicitly satisfied when run as a user script
  42. // via GreaseMonkey/TamperMonkey; however, these need to be explicitly handled when using a different mode (e.g. firefox/android):
  43. //
  44. // - jQuery - user interface (REQUIRED)
  45. // - genetic-js - genetic programming (OPTIONAL/EXPERIMENTAL)
  46. // - synaptic - neural networks (OPTIONAL/EXPERIMENTAL)
  47. //
  48. //
  49.  
  50. /* Here are some TODOs
  51. * - support RSS feeds http://dir.gmane.org/gmane.games.flightgear.devel/
  52. * - move event handling/processing to the CONFIG hash
  53. * - use try/catch more widely
  54. * - wrap function calls in try/call for better debugging/diagnostics
  55. * - add helpers for [].forEach.call, map, apply and call
  56. * - replace for/in, for/of, let statements for better compatibility (dont require ES6)
  57. * - for the same reason, replace use of functions with default params
  58. * - isolate UI (e.g. JQUERY) code in UserInterface hash
  59. * - expose regex/transformations via the UI
  60. *
  61. */
  62.  
  63. 'use strict';
  64.  
  65.  
  66. // TODO: move to GreaseMonkey/UI host
  67. // prevent conflicts with jQuery used on webpages: https://wiki.greasespot.net/Third-Party_Libraries#jQuery
  68. // http://stackoverflow.com/a/5014220
  69. this.$ = this.jQuery = jQuery.noConflict(true);
  70.  
  71. // this hash is just intended to help isolate UI specifics
  72. // so that we don't need to maintain/port tons of code
  73.  
  74. var UserInterface = {
  75. get: function() {
  76. return UserInterface.DEFAULT;
  77. },
  78. CONSOLE: {
  79. }, // CONSOLE (shell, mainly useful for testing)
  80. DEFAULT: {
  81. alert: function(msg) {return window.alert(msg); },
  82. prompt: function(msg) {return window.prompt(msg); },
  83. confirm: function(msg) {return window.confirm(msg); },
  84. dialog: null,
  85. selection: null,
  86. populateWatchlist: function() {
  87. },
  88. populateEditSections: function() {
  89. }
  90. }, // default UI mapping (Browser/User script)
  91. JQUERY: {
  92. } // JQUERY
  93. }; // UserInterface
  94.  
  95. var UI = UserInterface.get(); // DEFAULT for now
  96.  
  97.  
  98. // This hash is intended to help encapsulate platform specifics (browser/scripting host)
  99. // Ideally, all APIs that are platform specific should be kept here
  100. // This should make it much easier to update/port and maintain the script in the future
  101. var Environment = {
  102. getHost: function(xpi=false) {
  103. if(xpi) {
  104. Environment.scriptEngine = 'firefox addon';
  105. console.log('in firefox xpi/addon mode');
  106. return Environment.FirefoxAddon; // HACK for testing the xpi mode (firefox addon)
  107. }
  108. // 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
  109. if (typeof(GM_info) === 'undefined') {
  110. Environment.scriptEngine = "plain Chrome (Or Opera, or scriptish, or Safari, or rarer)";
  111. // See http://stackoverflow.com/a/2401861/331508 for optional browser sniffing code.
  112. }
  113. else {
  114. Environment.scriptEngine = GM_info.scriptHandler || "Greasemonkey";
  115. }
  116. console.log ('Instant cquotes is running on ' + Environment.scriptEngine + '.');
  117. //console.log("not in firefox addon mode...");
  118. // See also: https://wiki.greasespot.net/Cross-browser_userscripting
  119. return Environment.GreaseMonkey; // return the only/default host (for now)
  120. },
  121. validate: function(host) {
  122. if (host.get_persistent('startup.disable_validation',false)) return;
  123. if(Environment.scriptEngine !== "Greasemonkey")
  124. console.log("NOTE: This script has not been tested with script engines other than GreaseMonkey recently!");
  125. var dependencies = [
  126. {name:'jQuery', test: function() {} },
  127. {name:'genetic.js', test: function() {} },
  128. {name:'synaptic', test: function() {} },
  129. ];
  130. [].forEach.call(dependencies, function(dep) {
  131. console.log("Checking for dependency:"+dep.name);
  132. var status=false;
  133. try {
  134. dep.test.call(undefined);
  135. status=true;
  136. }
  137. catch(e) {
  138. status=false;
  139. }
  140. finally {
  141. var success = (status)?'==> success':'==> failed';
  142. console.log(success);
  143. return status;
  144. }
  145. });
  146. }, // validate
  147. // this contains unit tests for checking crucial APIs that must work for the script to work correctly
  148. // for the time being, most of these are stubs waiting to be filled in
  149. // for a working example, refer to the JSON test at the end
  150. // TODO: add jQuery tests
  151. APITests: [
  152. {name:'download', test: function(recipient) {recipient(true);} },
  153. {name:'make_doc', test: function(recipient) { recipient(true);} },
  154. {name:'eval_xpath', test: function(recipient) { recipient(true);} },
  155. {name:'JSON de/serialization', test: function(recipient) {
  156. //console.log("running json test");
  157. var identifier = 'unit_tests.json_serialization';
  158. var hash1 = {x:1,y:2,z:3};
  159. Host.set_persistent(identifier, hash1, true);
  160. var hash2 = Host.get_persistent(identifier,null,true);
  161. recipient(JSON.stringify(hash1) === JSON.stringify(hash2));
  162. } // callback
  163. },
  164. // downloads a posting and tries to transform it to 3rd person speech ...
  165. // TODO: add another test to check forum postings
  166. {name:'text/speech transformation', test: function(recipient) {
  167. // the posting we want to download
  168. var url='https://sourceforge.net/p/flightgear/mailman/message/35066974/';
  169. Host.downloadPosting(url, function (result) {
  170. // only process the first sentence by using comma/dot as delimiter
  171. var firstSentence = result.content.substring(result.content.indexOf(',')+1, result.content.indexOf('.'));
  172. var transformed = transformSpeech(firstSentence, result.author, null, speechTransformations );
  173. console.log("3rd person speech transformation:\n"+transformed);
  174. recipient(true);
  175. }); // downloadPosting()
  176. }// test()
  177. }, // end of speech transform test
  178. {
  179. name:"download $FG_ROOT/options.xml", test: function(recipient) {
  180. downloadOptionsXML();
  181. recipient(true);
  182. } // test
  183. }
  184. ], // end of APITests
  185. runAPITests: function(host, recipient) {
  186. console.log("Running API tests");
  187. for(let test of Environment.APITests ) {
  188. //var test = Environment.APITests[t];
  189. // 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
  190. recipient.call(undefined, test);
  191. } // foreach test
  192. }, // runAPITests
  193. /*
  194. * ===================================================================================================================================================
  195. *
  196. */
  197. // NOTE: This mode/environment is WIP and highly experimental ...
  198. // To see this working, you need to package up the whole file as a firefox xpi using "jpm xpi"
  199. // and then start the whole thing via "jpm run", to do that, you also need a matching package.json (i.e. via jpm init)
  200. // ALSO: you will have to explicitly install any dependencies using jpm
  201. FirefoxAddon: {
  202. init: function() {
  203. console.log("Firefox addon mode ...");
  204. },
  205. getScriptVersion: function() {
  206. return '0.36'; // FIXME
  207. },
  208. dbLog: function(msg) {
  209. console.log(msg);
  210. },
  211. addEventListener: function(ev, cb) {
  212.  
  213. require("sdk/tabs").on("ready", logURL);
  214. function logURL(tab) {
  215. console.log("URL loaded:" + tab.url);
  216. }
  217. },
  218. registerConfigurationOption: function(name, callback, hook) {
  219. // https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Add_a_Context_Menu_Item
  220. console.log("config menu support n/a in firefox mode");
  221. // https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Using_third-party_modules_%28jpm%29
  222. var menuitems = require("menuitem");
  223. var menuitem = menuitems.Menuitem({
  224. id: "clickme",
  225. menuid: "menu_ToolsPopup",
  226. label: name,
  227. onCommand: function() {
  228. console.log("menuitem clicked:");
  229. callback();
  230. },
  231. insertbefore: "menu_pageInfo"
  232. });
  233. },
  234. registerTrigger: function() {
  235. // https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Add_a_Context_Menu_Item
  236. // https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/context-menu#Item%28options%29
  237. var contextMenu = require("sdk/context-menu");
  238. var menuItem = contextMenu.Item({
  239. label: "Instant Cquote",
  240. context: contextMenu.SelectionContext(),
  241. // https://developer.mozilla.org/en/Add-ons/SDK/Guides/Two_Types_of_Scripts
  242. // https://developer.mozilla.org/en-US/Add-ons/SDK/Guides/Content_Scripts
  243. contentScript: 'self.on("click", function () {' +
  244. ' var text = window.getSelection().toString();' +
  245. ' self.postMessage(text);' +
  246. '});',
  247. onMessage: function (selectionText) {
  248. console.log(selectionText);
  249. instantCquote(selectionText);
  250. }
  251. });
  252. // for selection handling stuff, see: https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/selection
  253. function myListener() {
  254. console.log("A selection has been made.");
  255. }
  256. var selection = require("sdk/selection");
  257. selection.on('select', myListener);
  258. }, //registerTrigger
  259. get_persistent: function(key, default_value) {
  260. // https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/simple-storage
  261. var ss = require("sdk/simple-storage");
  262. console.log("firefox mode does not yet have persistence support");
  263. return default_value;},
  264. set_persistent: function(key, value) {
  265. console.log("firefox persistence stubs not yet filled in !");
  266. },
  267. set_clipboard: function(content) {
  268. // https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/clipboard
  269. //console.log('clipboard stub not yet filled in ...');
  270. var clipboard = require("sdk/clipboard");
  271. clipboard.set(content);
  272. } //set_cliipboard
  273. }, // end of FireFox addon config
  274. // placeholder for now ...
  275. Android: {
  276. // NOP
  277. }, // Android
  278.  
  279. ///////////////////////////////////////
  280. // supported script engines:
  281. ///////////////////////////////////////
  282. GreaseMonkey: {
  283. // TODO: move environment specific initialization code here
  284. init: function() {
  285. // Check if Greasemonkey/Tampermonkey is available
  286. try {
  287. // TODO: add version check for clipboard API and check for TamperMonkey/Scriptish equivalents ?
  288. GM_addStyle(GM_getResourceText('jQUI_CSS'));
  289. } // try
  290. catch (error) {
  291. console.log('Could not add style or determine script version');
  292. } // catch
  293.  
  294. var commands = [
  295. {name:'Setup quotes',callback:setupDialog, hook:'S' },
  296. {name:'Check quotes',callback:selfCheckDialog, hook:'C' }
  297. ];
  298. for (let c of commands ) {
  299. this.registerConfigurationOption(c.name, c.callback, c.hook);
  300. }
  301. }, // init()
  302. getScriptVersion: function() {
  303. return GM_info.script.version;
  304. },
  305. dbLog: function (message) {
  306. if (Boolean(DEBUG)) {
  307. console.log('Instant cquotes:' + message);
  308. }
  309. }, // dbLog()
  310. registerConfigurationOption: function(name,callback,hook) {
  311. // https://wiki.greasespot.net/GM_registerMenuCommand
  312. // https://wiki.greasespot.net/Greasemonkey_Manual:Monkey_Menu#The_Menu
  313. GM_registerMenuCommand(name, callback, hook);
  314. }, //registerMenuCommand()
  315. registerTrigger: function() {
  316. // TODO: we can use the following callback non-interactively, i.e. to trigger background tasks
  317. // http://javascript.info/tutorial/onload-ondomcontentloaded
  318. document.addEventListener("DOMContentLoaded", function(event) {
  319. console.log("Instant Cquotes: DOM fully loaded and parsed");
  320. });
  321.  
  322. window.addEventListener('load', init); // page fully loaded
  323. Host.dbLog('Instant Cquotes: page load handler registered');
  324.  
  325. // Initialize (matching page loaded)
  326. function init() {
  327. console.log('Instant Cquotes: page load handler invoked');
  328. var profile = getProfile();
  329. Host.dbLog("Profile type is:"+profile.type);
  330. // Dispatch to correct event handler (depending on website/URL)
  331. // TODO: this stuff could/should be moved into the config hash itself
  332. if (profile.type=='wiki') {
  333. profile.event_handler(); // just for testing
  334. return;
  335. }
  336. Host.dbLog('using default mode');
  337. document.onmouseup = instantCquote;
  338. // HACK: preparations for moving the the event/handler logic also into the profile hash, so that the wiki (edit mode) can be handled equally
  339. //eval(profile.event+"=instantCquote");
  340. } // init()
  341.  
  342.  
  343. }, // registerTrigger
  344.  
  345. download: function (url, callback, method='GET') {
  346. // http://wiki.greasespot.net/GM_xmlhttpRequest
  347. try {
  348. GM_xmlhttpRequest({
  349. method: method,
  350. url: url,
  351. onload: callback
  352. });
  353. }catch(e) {
  354. console.log("download did not work");
  355. }
  356. }, // download()
  357. // is only intended to work with archives supported by the hash
  358. downloadPosting: function (url, EventHandler) {
  359. Host.download(url, function (response) {
  360. var profile = getProfile(url);
  361. var blob = response.responseText;
  362. var doc = Host.make_doc(blob,'text/html');
  363. var result = {}; // hash to be returned
  364. [].forEach.call(['author','date','title','content'], function(field) {
  365. var xpath_query = '//' + profile[field].xpath;
  366. try {
  367. var value = Host.eval_xpath(doc, xpath_query).stringValue;
  368. //UI.alert("extracted field value:"+value);
  369. // now apply all transformations, if any
  370. value = applyTransformations(value, profile[field].transform );
  371. result[field]=value; // store the extracted/transormed value in the hash that we pass on
  372. } // try
  373. catch(e) {
  374. UI.alert("downloadPosting failed:\n"+ e.message);
  375. } // catch
  376. }); // forEach field
  377. EventHandler(result); // pass the result to the handler
  378. }); // call to Host.download()
  379. }, // downloadPosting()
  380. // TODO: add makeAJAXCall, and makeWikiCall here
  381.  
  382. // turn a string/text blob into a DOM tree that can be queried (e.g. for xpath expressions)
  383. // FIXME: this is browser specific not GM specific ...
  384. make_doc: function(text, type='text/html') {
  385. // to support other browsers, see: https://developer.mozilla.org/en/docs/Web/API/DOMParser
  386. return new DOMParser().parseFromString(text,type);
  387. }, // make DOM document
  388. // xpath handling may be handled separately depending on browser/platform, so better encapsulate this
  389. // FIXME: this is browser specific not GM specific ...
  390. eval_xpath: function(doc, xpath, type=XPathResult.STRING_TYPE) {
  391. return doc.evaluate(xpath, doc, null, type, null);
  392. }, // eval_xpath
  393. set_persistent: function(key, value, json=false)
  394. {
  395. // transparently stringify to json
  396. if(json) {
  397. // http://stackoverflow.com/questions/16682150/store-a-persistent-list-between-sessions
  398. value = JSON.stringify (value);
  399. }
  400. // https://wiki.greasespot.net/GM_setValue
  401. GM_setValue(key, value);
  402. //UI.alert('Saved value for key\n'+key+':'+value);
  403. }, // set_persistent
  404. get_persistent: function(key, default_value, json=false) {
  405. // https://wiki.greasespot.net/GM_getValue
  406. var value=GM_getValue(key, default_value);
  407. // transparently support JSON: http://stackoverflow.com/questions/16682150/store-a-persistent-list-between-sessions
  408. if(json) {
  409. value = JSON.parse (value) || {};
  410. }
  411. return value;
  412. }, // get_persistent
  413.  
  414. setClipboard: function(msg) {
  415. // this being a greasemonkey user-script, we are not
  416. // subject to usual browser restrictions
  417. // http://wiki.greasespot.net/GM_setClipboard
  418. GM_setClipboard(msg);
  419. }, // setClipboard()
  420. getTemplate: function() {
  421. // hard-coded default template
  422. var template = '$CONTENT<ref>{{cite web\n' +
  423. ' |url = $URL \n' +
  424. ' |title = <nowiki> $TITLE </nowiki> \n' +
  425. ' |author = <nowiki> $AUTHOR </nowiki> \n' +
  426. ' |date = $DATE \n' +
  427. ' |added = $ADDED \n' +
  428. ' |script_version = $SCRIPT_VERSION \n' +
  429. ' }}</ref>\n';
  430. // return a saved template if found, fall back to hard-coded one above otherwise
  431. return Host.get_persistent('default_template', template);
  432. } // getTemplate
  433.  
  434. } // end of GreaseMonkey environment, add other environments below
  435. }; // Environment hash - intended to help encapsulate host specific stuff (APIs)
  436.  
  437.  
  438. // the first thing we need to do is to determine what APIs are available
  439. // and store everything in a Host hash, which is subsequently used for API lookups
  440. // the Host hash contains all platform/browser-specific APIs
  441. var Host = Environment.getHost();
  442. Environment.validate(Host); // this checks the obtained host to see if all required dependencies are available
  443. Host.init(); // run environment specific initialization code (e.g. logic for GreaseMonkey setup)
  444.  
  445.  
  446. // move DEBUG handling to a persistent configuration flag so that we can configure this using a jQuery dialog (defaulted to false)
  447. // TODO: move DEBUG variable to Environment hash / init() routine
  448. var DEBUG = Host.get_persistent('debug_mode_enabled', false);
  449. Host.dbLog("Debug mode is:"+DEBUG);
  450. function DEBUG_mode() {
  451. // reset script invocation counter for testing purposes
  452. Host.dbLog('Resetting script invocation counter');
  453. Host.set_persistent(GM_info.script.version, 0);
  454. }
  455.  
  456.  
  457. if (DEBUG)
  458. DEBUG_mode();
  459.  
  460. // hash with supported websites/URLs, includes xpath and regex expressions to extract certain fields, and a vector with optional transformations for post-processing each field
  461.  
  462. var CONFIG = {
  463. // 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
  464. // for editing the FlightGear wiki
  465. 'FlightGear.wiki': {
  466. type: 'wiki',
  467. enabled: false,
  468. event: 'document.onmouseup', // when to invoke the event handler
  469. // TODO: move downloadWatchlist() etc here
  470. event_handler: function () {
  471. console.log('FlightGear wiki handler active (waiting to be populated)');
  472. // this is where the logic for a wiki mode can be added over time (for now, it's a NOP)
  473. //for each supported mode, invoke the trigger and call the corresponding handler
  474. [].forEach.call(CONFIG['FlightGear.wiki'].modes, function(mode) {
  475. //dbLog("Checking trigger:"+mode.name);
  476. if(mode.trigger() ) {
  477. mode.handler();
  478. }
  479. });
  480. }, // the event handler to be invoked
  481. url_reg: '^(http|https)://wiki.flightgear.org', // ignore for now: not currently used by the wiki mode
  482. modes: [
  483. { name:'process-editSections',
  484. trigger: function() {return true;}, // match URL regex - return true for always match
  485. // the code implementing the mode
  486. handler: function() {
  487. var editSections = document.getElementsByClassName('mw-editsection');
  488. console.log('FlightGear wiki article, number of edit sections: '+editSections.length);
  489. // for now, just rewrite edit sections and add a note to them
  490. [].forEach.call(editSections, function (sec) {
  491. sec.appendChild(
  492. document.createTextNode(' (instant-cquotes is lurking) ')
  493. );
  494. }); //forEach section
  495. } // handler
  496. } // process-editSections
  497. // TODO: add other wiki modes below
  498. ] // modes
  499. }, // end of wiki profile
  500. 'Sourceforge Mailing list': {
  501. enabled: true,
  502. type: 'archive',
  503. event: 'document.onmouseup', // when to invoke the event handler
  504. event_handler: instantCquote, // the event handler to be invoked
  505. url_reg: '^(http|https)://sourceforge.net/p/flightgear/mailman/.*/',
  506. content: {
  507. xpath: 'tbody/tr[2]/td/pre/text()', // NOTE this is only used by the downloadPosting helper to retrieve the posting without having a selection (TODO:add content xpath to forum hash)
  508. selection: getSelectedText,
  509. idStyle: /msg[0-9]{8}/,
  510. parentTag: [
  511. 'tagName',
  512. 'PRE'
  513. ],
  514. transform: [],
  515. }, // content recipe
  516. // vector with tests to be executed for sanity checks (unit testing)
  517. tests: [
  518. {
  519. url: 'https://sourceforge.net/p/flightgear/mailman/message/35059454/',
  520. author: 'Erik Hofman',
  521. date: 'May 3rd, 2016', // NOTE: using the transformed date here
  522. title: 'Re: [Flightgear-devel] Auto altimeter setting at startup (?)'
  523. },
  524. {
  525. url: 'https://sourceforge.net/p/flightgear/mailman/message/35059961/',
  526. author: 'Ludovic Brenta',
  527. date: 'May 3rd, 2016',
  528. title: 'Re: [Flightgear-devel] dual-control-tools and the limit on packet size'
  529. },
  530. {
  531. url: 'https://sourceforge.net/p/flightgear/mailman/message/20014126/',
  532. author: 'Tim Moore',
  533. date: 'Aug 4th, 2008',
  534. title: 'Re: [Flightgear-devel] Cockpit displays (rendering, modelling)'
  535. },
  536. {
  537. url: 'https://sourceforge.net/p/flightgear/mailman/message/23518343/',
  538. author: 'Tim Moore',
  539. date: 'Sep 10th, 2009',
  540. title: '[Flightgear-devel] Atmosphere patch from John Denker'
  541. } // add other tests below
  542.  
  543. ], // end of vector with self-tests
  544. // regex/xpath and transformations for extracting various required fields
  545. author: {
  546. xpath: 'tbody/tr[1]/td/div/small/text()',
  547. transform: [extract(/From: (.*) <.*@.*>/)]
  548. },
  549. title: {
  550. xpath: 'tbody/tr[1]/td/div/div[1]/b/a/text()',
  551. transform:[]
  552. },
  553. date: {
  554. xpath: 'tbody/tr[1]/td/div/small/text()',
  555. transform: [extract(/- (.*-.*-.*) /)]
  556. },
  557. url: {
  558. xpath: 'tbody/tr[1]/td/div/div[1]/b/a/@href',
  559. transform: [prepend('https://sourceforge.net')]
  560. }
  561. }, // end of mailing list profile
  562. // next website/URL (forum)
  563. 'FlightGear forum': {
  564. enabled: true,
  565. type: 'archive',
  566. event: 'document.onmouseup', // when to invoke the event handler (not used atm)
  567. event_handler: null, // the event handler to be invoked (not used atm)
  568. url_reg: /https:\/\/forum\.flightgear\.org\/.*/,
  569. content: {
  570. xpath: '', //TODO: this must be added for downloadPosting() to work, or it cannot extract contents
  571. selection: getSelectedHtml,
  572. idStyle: /p[0-9]{6}/,
  573. parentTag: [
  574. 'className',
  575. 'content',
  576. 'postbody'
  577. ],
  578. transform: [
  579. removeComments,
  580. forum_quote2cquote,
  581. forum_smilies2text,
  582. forum_fontstyle2wikistyle,
  583. forum_code2syntaxhighlight,
  584. img2link,
  585. a2wikilink,
  586. vid2wiki,
  587. list2wiki,
  588. forum_br2newline
  589. ]
  590. },
  591. // vector with tests to be executed for sanity checks (unit testing)
  592. // postings will be downloaded using the URL specified, and then the author/title
  593. // fields extracted using the outer regex and matched against what is expected
  594. // NOTE: forum postings can be edited, so that these tests would fail - thus, it makes sense to pick locked topics/postings for such tests
  595. tests: [
  596. {
  597. url: 'https://forum.flightgear.org/viewtopic.php?f=18&p=284108#p284108',
  598. author: 'mickybadia',
  599. date: 'May 3rd, 2016',
  600. title: 'OSM still PNG maps'
  601. },
  602. {
  603. url: 'https://forum.flightgear.org/viewtopic.php?f=19&p=284120#p284120',
  604. author: 'Thorsten',
  605. date: 'May 3rd, 2016',
  606. title: 'Re: FlightGear\'s Screenshot Of The Month MAY 2016'
  607. },
  608. {
  609. url: 'https://forum.flightgear.org/viewtopic.php?f=71&t=29279&p=283455#p283446',
  610. author: 'Hooray',
  611. date: 'Apr 25th, 2016',
  612. title: 'Re: Best way to learn Canvas?'
  613. },
  614. {
  615. url: 'https://forum.flightgear.org/viewtopic.php?f=4&t=1460&p=283994#p283994',
  616. author: 'bugman',
  617. date: 'May 2nd, 2016',
  618. title: 'Re: eurofighter typhoon'
  619. } // add other tests below
  620.  
  621. ], // end of vector with self-tests
  622. author: {
  623. xpath: 'div/div[1]/p/strong/a/text()',
  624. transform: [] // no transformations applied
  625. },
  626. title: {
  627. xpath: 'div/div[1]/h3/a/text()',
  628. transform: [] // no transformations applied
  629. },
  630. date: {
  631. xpath: 'div/div[1]/p/text()[2]',
  632. transform: [extract(/» (.*?[0-9]{4})/)]
  633. },
  634. url: {
  635. xpath: 'div/div[1]/p/a/@href',
  636. transform: [
  637. extract(/\.(.*)/),
  638. prepend('https://forum.flightgear.org')
  639. ] // transform vector
  640. } // url
  641. } // forum
  642. }; // CONFIG has
  643.  
  644. // hash to map URLs (wiki article, issue tracker, sourceforge link, forum thread etc) to existing wiki templates
  645. var MatchURL2Templates = [
  646. // placeholder for now
  647. {
  648. name: 'rewrite sourceforge code links',
  649. url_reg: '',
  650. handler: function() {
  651. } // handler
  652. } // add other templates below
  653. ]; // MatchURL2Templates
  654.  
  655.  
  656.  
  657.  
  658. // output methods (alert and jQuery for now)
  659. var OUTPUT = {
  660. // Shows a window.prompt() message box
  661. msgbox: function (msg) {
  662. UI.prompt('Copy to clipboard ' + Host.getScriptVersion(), msg);
  663. Host.setClipboard(msg);
  664. }, // msgbox
  665. // this is currently work-in-progress, and will need to be refactored sooner or later
  666. // for now, functionality matters more than elegant design/code :)
  667. jQueryTabbed: function(msg, original) {
  668. // FIXME: using backtics here makes the whole thing require ES6 ....
  669. var markup = $(`<div id="tabs">
  670. <ul>
  671. <li><a href="#selection">Selection</a></li>
  672. <li><a href="#articles">Articles</a></li>
  673. <li><a href="#templates">Templates</a></li>
  674. <li><a href="#development">Development</a></li>
  675. <li><a href="#settings">Settings</a></li>
  676. <li><a href="#help">Help</a></li>
  677. <li><a href="#about">About</a></li>
  678. </ul>
  679. <div id="selection">This tab contains your extracted and post-processed selection, converted to proper wikimedia markup, including proper attribution.
  680. <div id="content">
  681.  
  682. <label for="template_select">Select a template</label>
  683. <select name="template_select" id="template_select">
  684. <option>default</option>
  685. <option>cquote</option>
  686. </select>
  687.  
  688. </div>
  689. <div id="options">
  690. <b>Note this is work-in-progress, i.e. not yet fully functional</b><br/>
  691. <label for="article_select">Select an article to update</label>
  692. <select name="article_select" id="article_select">
  693. <optgroup id="news" label="News"/>
  694. <optgroup id="support" label="Support"/>
  695. <optgroup id="release" label="Release"/>
  696. <optgroup id="develop" label="Development"/>
  697. <optgroup id="watchlist" label="Watchlist"/>
  698. </select>
  699. <p/>
  700. <label for="section_select">Select section:</label>
  701. <select name="section_select" id="section_select">
  702. </select>
  703. </div>
  704. </div>
  705. <div id="articles">This tab contains articles that you can directly access/edit using the mediawiki API<br/>
  706. Note: The watchlist is retrieved dynamically, so does not need to be edited here<br/>
  707. <label for="article_select">Select an article</label>
  708. <select name="article_select" id="article_select">
  709. <optgroup id="news" label="News"/>
  710. <optgroup id="support" label="Support"/>
  711. <optgroup id="develop" label="Development"/>
  712. <optgroup id="release" label="Release"/>
  713. <!-- the watchlist is retrieved dynamically, so omit it here
  714. <optgroup id="watchlist" label="Watchlist"/>
  715. -->
  716. </select>
  717.  
  718. <button id="article_new">New</button>
  719. <button id="article_remove">Remove</button>
  720.  
  721. <div id="edit_article">
  722. <label for="article_name">Article</label>
  723. <input type="text" id="article_name" name="article_name"><br/>
  724.  
  725. <label for="article_url">Link</label>
  726. <input type="text" id="article_url" name="article_url"><br/>
  727.  
  728. <button id="article_save">Save</button>
  729. </div>
  730.  
  731. </div>
  732. <div id="templates">This tab contains templates for different types of articles (newsletter, changelog, release plan etc)<p/>
  733. For now, this is WIP - in the future, there will be a dropdown menu added and all templates will be editable.<p/>
  734. <div id="template_header">
  735.  
  736. <label for="template_select">Select a template</label>
  737. <select name="template_select" id="template_select">
  738. <option>default</option>
  739. <option>cquote</option>
  740. </select>
  741.  
  742. </div>
  743. <div id="template_area"/>
  744. <div id="template_controls">
  745. <button id="template_save">Save</button>
  746. </div>
  747. </div>
  748. <div id="development">This tab is a placeholder for features currently under development<p/>
  749. <button id="evolve_regex">Evolve regex</button><p/>
  750. <button id="test_perceptron">Test Perceptron</button><p/>
  751. <div id="output">
  752.  
  753. <table id="results">
  754. <thead>
  755. <tr>
  756. <th>Generation</th>
  757. <th>Fitness</th>
  758. <th>Expression</th>
  759. <th>Result</th>
  760. </tr>
  761. </thead>
  762. <tbody>
  763. </tbody>
  764. </table>
  765.  
  766. <!--
  767. <textarea id="devel_output" lines="10"></textarea><p/>
  768. -->
  769. </div>
  770. </div>
  771.  
  772. <div id="settings">This tab will contain script specific settings
  773. </div>
  774. <div id="help">One day, this tab may contain help....<p/><button id="helpButton">Instant Cquotes</button>
  775. </div>
  776. <div id="about">show some script related information here
  777. </div>
  778. </div>`); // tabs div
  779. var evolve_regex = $('div#development button#evolve_regex', markup);
  780. evolve_regex.click(function() {
  781. //alert("Evolve regex");
  782. evolve_expression_test();
  783. });
  784. var test_perceptron = $('div#development button#test_perceptron', markup);
  785. test_perceptron.click(function() {
  786. alert("Test perceptron");
  787. });
  788. // add dynamic elements to each tab
  789. // NOTE: this affects all template selectors, on all tabs
  790. $('select#template_select', markup).change(function() {
  791. UI.alert("Sorry, templates are not yet fully implemented (WIP)");
  792. });
  793. var help = $('#helpButton', markup);
  794. help.button();
  795. help.click(function() {
  796. window.open("http://wiki.flightgear.org/FlightGear_wiki:Instant-Cquotes");
  797. });
  798. // rows="10"cols="80" style=" width: 420px; height: 350px"
  799. var textarea = $('<textarea id="quotedtext" rows="20" cols="70"/>');
  800. textarea.val(msg);
  801. $('#selection #content', markup).append(textarea);
  802. var templateArea = $('<textarea id="template-edit" rows="20" cols="70"/>');
  803. templateArea.val( Host.getTemplate() );
  804. $('div#templates div#template_area', markup).append(templateArea);
  805. //$('#templates', markup).append($('<button>'));
  806. $('div#templates div#template_controls button#template_save',markup).button().click(function() {
  807. //UI.alert("Saving template:\n"+templateArea.val() );
  808. Host.set_persistent('default_template',templateArea.val() );
  809. }); // save template
  810. // TODO: Currently, this is hard-coded, but should be made customizable via the "articles" tab at some point ...
  811. var articles = [
  812. // NOTE: category must match an existing <optgroup> above, title must match an existing wiki article
  813. {category:'support', name:'Frequently asked questions', url:''},
  814. {category:'support', name:'Asking for help', url:''},
  815. {category:'news', name:'Next newsletter', url:''},
  816. {category:'news', name:'Next changelog', url:''},
  817. {category:'release', name:'Release plan/Lessons learned', url:''}, // TODO: use wikimedia template
  818. {category:'develop', name:'Nasal library', url:''},
  819. {category:'develop', name:'Canvas Snippets', url:''},
  820. ];
  821. // TODO: this should be moved elsewhere
  822. function updateArticleList(selector) {
  823. $.each(articles, function (i, article) {
  824. $(selector+ ' optgroup#'+article.category, markup).append($('<option>', {
  825. value: article.name, // FIXME: just a placeholder for now
  826. text : article.name
  827. })); //append option
  828. }); // foreach
  829. } // updateArticleList
  830. // add the article list to the corresponding dropdown menus
  831. updateArticleList('select#article_select');
  832. // populate watchlist (prototype for now)
  833. // TODO: generalize & refactor: url, format
  834. // https://www.mediawiki.org/wiki/API:Watchlist
  835. // http://wiki.flightgear.org/api.php?action=query&list=watchlist
  836. var watchlist_url = 'http://wiki.flightgear.org/api.php?action=query&list=watchlist&format=json';
  837. Host.download(watchlist_url, function(response) {
  838. try {
  839. var watchlist = JSON.parse(response.responseText);
  840. //$('div#options select#section_select', markup).empty(); // delete all sections
  841. $.each(watchlist.query.watchlist, function (i, article) {
  842. $('div#options select#article_select optgroup#watchlist', markup).append($('<option>', {
  843. value: article.title, //FIXME just a placeholder for now
  844. text : article.title
  845. }));
  846. }); //foreach section
  847.  
  848. }
  849. catch (e) {
  850. UI.alert(e.message);
  851. }
  852. }); // download & populate watchlist
  853. // register an event handler for the main tab, so that article specific sections can be retrieved
  854. $('div#options select#article_select', markup).change(function() {
  855. var article = this.value;
  856. // HACK: try to get a login token (actually not needed just for reading ...)
  857. Host.download('http://wiki.flightgear.org/api.php?action=query&prop=info|revisions&intoken=edit&rvprop=timestamp&titles=Main%20Page', function (response) {
  858. var message = 'FlightGear wiki login status (AJAX):';
  859. var status = response.statusText;
  860. // populate dropdown menu with article sections
  861. if (status === 'OK') {
  862. // Resolve redirects: https://www.mediawiki.org/wiki/API:Query#Resolving_redirects
  863. var section_url = 'http://wiki.flightgear.org/api.php?action=parse&page='+encodeURIComponent(article)+'&prop=sections&format=json&redirects';
  864. Host.download(section_url, function(response) {
  865. try {
  866. var sections = JSON.parse(response.responseText);
  867. $('div#options select#section_select', markup).empty(); // delete all sections
  868. $.each(sections.parse.sections, function (i, section) {
  869. $('div#options select#section_select', markup).append($('<option>', {
  870. value: section.line, //FIXME just a placeholder for now
  871. text : section.line
  872. }));
  873. }); //foreach section
  874.  
  875. }
  876. catch (e) {
  877. UI.alert(e.message);
  878. }
  879. }); //download sections
  880. } // login status is OK
  881.  
  882. }); // Host.download() call, i.e. we have a login token
  883. }); // on select change
  884. // init the tab stuff
  885. markup.tabs();
  886. var diagParam = {
  887. title: 'Instant Cquotes ' + Host.getScriptVersion(),
  888. modal: true,
  889. width: 700,
  890. buttons: [
  891. {
  892. text:'reported speech',
  893. click: function() {
  894. textarea.val(createCquote(original,true));
  895. }
  896. },
  897. {
  898. text: 'Copy',
  899. click: function () {
  900. Host.setClipboard(msg);
  901. $(this).dialog('close');
  902. }
  903. }
  904. ]
  905. };
  906. // actually show our tabbed dialog using the params above
  907. markup.dialog(diagParam);
  908. } // jQueryTabbed()
  909. }; // output methods
  910.  
  911. //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  912. // TODO: we can use an online API to help with some of this: http://www.eslnow.org/reported-speech-converter/
  913. // See also: http://blog.mashape.com/list-of-25-natural-language-processing-apis/
  914. // http://text-processing.com/docs/phrases.html
  915. // http://www.alchemyapi.com/
  916. // https://words.bighugelabs.com/api.php
  917. // https://www.wordsapi.com/
  918. // http://www.dictionaryapi.com/
  919. // https://www.textrazor.com/
  920. // http://www.programmableweb.com/news/how-5-natural-language-processing-apis-stack/analysis/2014/07/28
  921.  
  922. var speechTransformations = [
  923. // TODO: support aliasing using vectors: would/should
  924. // ordering is crucial here (most specific first, least specific/most generic last)
  925. // first, we start off by expanding short forms: http://www.learnenglish.de/grammar/shortforms.html
  926. // http://www.macmillandictionary.com/thesaurus-category/british/short-forms
  927. {query:/couldn\'t/gi, replacement:'could not'},
  928. {query:/I could not/gi, replacement:'$author could not'},
  929. {query:/I\'m/gi, replacement:'I am'},
  930. {query:/I am/gi, replacement:'$author is'},
  931. {query:/I\'ve/, replacement:'I have'},
  932. {query:/I have had/, replacement:'$author had'},
  933. {query:/can(\'|\’)t/gi, replacement:'cannot'},
  934. {query:/I(\'|\’)ll/gi, replacement:'$author will'},
  935. {query:/I(\'|\’)d/gi, replacement:'$author would'},
  936. {query:/I have done/gi, replacement:'$author has done'},
  937. {query:/I\'ve done/gi, replacement:'$author has done'}, //FIXME. queries should really be vectors ...
  938. {query:/I believe/gi, replacement:'$author suggested'},
  939. {query:/I think/gi, replacement:'$author suggested'},
  940. {query:/I guess/gi, replacement:'$author believes'},
  941. {query:/I can see that/gi, replacement:'$author suggested that'},
  942. {query:/I have got/gi, replacement:'$author has got'},
  943. {query:/I\'ve got/gi, replacement:'$author has got'},
  944. {query:/I\'d suggest/gi, replacement:'$author would suggest'},
  945. {query:/I\’m prototyping/gi, replacement:'$author is prototyping'},
  946. {query:/I myself/gi, replacement:'$author himself'},
  947. {query:/I am/gi, replacement:' $author is'},
  948. {query:/I can see/gi, replacement:'$author can see'},
  949. {query:/I can/gi, replacement:'$author can'},
  950. {query:/I have/gi, replacement:'$author has'},
  951. {query:/I should/g, replacement:'$author should'},
  952. {query:/I shall/gi, replacement:'$author shall'},
  953. {query:/I may/gi, replacement:'$author may'},
  954. {query:/I will/gi, replacement:'$author will'},
  955. {query:/I would/gi, replacement:'$author would'},
  956. {query:/by myself/gi, replacement:'by $author'},
  957. {query:/and I/gi, replacement:'and $author'},
  958. {query:/and me/gi, replacement:'and $author'},
  959. {query:/and myself/gi, replacement:'and $author'}
  960. // least specific stuff last (broad/generic stuff is kept as is, with author clarification added in parentheses)
  961. /*
  962. {query:/I/, replacement:'I ($author)'},
  963. {query:/me/, replacement:'me ($author)'},
  964. {query:/my/, replacement:'my ($author)'},
  965. {query:/myself/, replacement:'myself ($author)'},
  966. {query:/mine/, replacement:'$author'}
  967. */
  968. ];
  969.  
  970. // try to assist in transforming speech using the transformation vector passed in
  971. // still needs to be exposed via the UI
  972. function transformSpeech(text, author, gender, transformations) {
  973. // WIP: foreach transformation in vector, replace the search pattern with the matched string (replacing author/gender as applicable)
  974. //alert("text to be transformed:\n"+text);
  975. for(var i=0;i< transformations.length; i++) {
  976. var token = transformations[i];
  977. // patch the replacement string using the correct author name
  978. var replacement = token.replacement.replace(/\$author/gi, author);
  979. text = text.replace(token.query, replacement);
  980. } // end of token transformation
  981. console.log("transformed text is:"+text);
  982. return text;
  983. } // transformSpeech
  984.  
  985. // run a self-test
  986.  
  987. (function() {
  988. var author ="John Doe";
  989. var transformed = transformSpeech("I have decided to commit a new feature", author, null, speechTransformations );
  990. if (transformed !== author+" has decided to commit a new feature")
  991. Host.dbLog("FIXME: Speech transformations are not working correctly");
  992. }) ();
  993. //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  994.  
  995. var MONTHS = [
  996. 'Jan',
  997. 'Feb',
  998. 'Mar',
  999. 'Apr',
  1000. 'May',
  1001. 'Jun',
  1002. 'Jul',
  1003. 'Aug',
  1004. 'Sep',
  1005. 'Oct',
  1006. 'Nov',
  1007. 'Dec'
  1008. ];
  1009. // Conversion for forum emoticons
  1010. var EMOTICONS = [
  1011. [/:shock:/g,
  1012. 'O_O'],
  1013. [
  1014. /:lol:/g,
  1015. '(lol)'
  1016. ],
  1017. [
  1018. /:oops:/g,
  1019. ':$'
  1020. ],
  1021. [
  1022. /:cry:/g,
  1023. ';('
  1024. ],
  1025. [
  1026. /:evil:/g,
  1027. '>:)'
  1028. ],
  1029. [
  1030. /:twisted:/g,
  1031. '3:)'
  1032. ],
  1033. [
  1034. /:roll:/g,
  1035. '(eye roll)'
  1036. ],
  1037. [
  1038. /:wink:/g,
  1039. ';)'
  1040. ],
  1041. [
  1042. /:!:/g,
  1043. '(!)'
  1044. ],
  1045. [
  1046. /:\?:/g,
  1047. '(?)'
  1048. ],
  1049. [
  1050. /:idea:/g,
  1051. '(idea)'
  1052. ],
  1053. [
  1054. /:arrow:/g,
  1055. '(->)'
  1056. ],
  1057. [
  1058. /:mrgreen:/g,
  1059. 'xD'
  1060. ]
  1061. ];
  1062. // ##################
  1063. // # Main functions #
  1064. // ##################
  1065.  
  1066.  
  1067. // the required trigger is host specific (userscript vs. addon vs. android etc)
  1068. // for now, this merely wraps window.load mapping to the instantCquotoe callback below
  1069. Host.registerTrigger();
  1070.  
  1071.  
  1072. // FIXME: function is currently referenced in CONFIG hash - event_handler, so cannot be easily moved across
  1073. // The main function
  1074. // TODO: split up, so that we can reuse the code elsewhere
  1075. function instantCquote(sel) {
  1076. var profile = getProfile();
  1077. // TODO: use config hash here
  1078. var selection = document.getSelection(),
  1079. post_id=0;
  1080. try {
  1081. post_id = getPostId(selection, profile);
  1082. }
  1083. catch (error) {
  1084. Host.dbLog('Failed extracting post id\nProfile:' + profile);
  1085. return;
  1086. }
  1087. if (selection.toString() === '') {
  1088. Host.dbLog('No text is selected, aborting function');
  1089. return;
  1090. }
  1091. if (!checkValid(selection, profile)) {
  1092. Host.dbLog('Selection is not valid, aborting function');
  1093. return;
  1094. }
  1095. try {
  1096. transformationLoop(profile, post_id);
  1097. }
  1098. catch(e) {
  1099. UI.alert("Transformation loop:\n"+e.message);
  1100. }
  1101. } // instantCquote
  1102.  
  1103. // TODO: this needs to be refactored so that it can be also reused by the async/AJAX mode
  1104. // to extract fields in the background (i.e. move to a separate function)
  1105. function transformationLoop(profile, post_id) {
  1106. var output = {}, field;
  1107. Host.dbLog("Starting extraction/transformation loop");
  1108. for (field in profile) {
  1109. if (field === 'name') continue;
  1110. if (field ==='type' || field === 'event' || field === 'event_handler') continue; // skip fields that don't contain xpath expressions
  1111. Host.dbLog("Extracting field using field id:"+post_id);
  1112. var fieldData = extractFieldInfo(profile, post_id, field);
  1113. var transform = profile[field].transform;
  1114. if (transform !== undefined) {
  1115. Host.dbLog('Field \'' + field + '\' before transformation:\n\'' + fieldData + '\'');
  1116. fieldData = applyTransformations(fieldData, transform);
  1117. Host.dbLog('Field \'' + field + '\' after transformation:\n\'' + fieldData + '\'');
  1118. }
  1119. output[field] = fieldData;
  1120. } // extract and transform all fields for the current profile (website)
  1121. Host.dbLog("extraction and transformation loop finished");
  1122. output.content = stripWhitespace(output.content);
  1123. var outputPlain = createCquote(output);
  1124. outputText(outputPlain, output);
  1125. } // transformationLoop()
  1126.  
  1127.  
  1128.  
  1129. /// #############
  1130.  
  1131. function runProfileTests() {
  1132. for (var profile in CONFIG) {
  1133. if (CONFIG[profile].type != 'archive' || !CONFIG[profile].enabled ) continue; // skip the wiki entry, because it's not an actual archive that we need to test
  1134. // should be really moved to downloadPostign
  1135. if (CONFIG[profile].content.xpath === '') console.log("xpath for content extraction is empty, cannot procedurally extract contents");
  1136. for (var test in CONFIG[profile].tests) {
  1137. var required_data = CONFIG[profile].tests[test];
  1138. var title = required_data.title;
  1139. //dbLog('Running test for posting titled:' + title);
  1140. // fetch posting via getPostingDataAJAX() and compare to the fields we are looking for (author, title, date)
  1141. //getPostingDataAJAX(profile, required_data.url);
  1142. //alert("required title:"+title);
  1143. } // foreach test
  1144.  
  1145. } // foreach profile (website)
  1146. } //runProfileTests
  1147.  
  1148. function selfCheckDialog() {
  1149. var sections = '<h3>Important APIs:</h3><div id="api_checks"></div>';
  1150.  
  1151.  
  1152. try {
  1153. runProfileTests.call(undefined); // check website profiles
  1154. }
  1155. catch (e) {
  1156. UI.alert(e.message);
  1157. }
  1158. for (var profile in CONFIG) {
  1159. // TODO: also check if enabled or not
  1160. if (CONFIG[profile].type != 'archive') continue; // skip the wiki entry, because it's not an actual archive that we need to test
  1161. var test_results = '';
  1162. for (var test in CONFIG[profile].tests) {
  1163. // var fieldData = extractFieldInfo(profile, post_id, 'author');
  1164. test_results += CONFIG[profile].tests[test].title + '<p/>';
  1165. }
  1166. sections +='<h3>' + profile + ':<font color="blue">'+ CONFIG[profile].url_reg+'</font></h3><div><p>' + test_results + '</p></div>\n';
  1167. } // https://jqueryui.com/accordion/
  1168. var checkDlg = $('<div id="selfCheck" title="Self Check dialog"><p><div id="accordion">' + sections + '</div></p></div>');
  1169. // run all API tests, invoke the callback to obtain the status
  1170. Environment.runAPITests(Host, function(meta) {
  1171. //console.log('Running API test '+meta.name);
  1172. meta.test(function(result) {
  1173. var status = (result)?'success':'fail';
  1174. var test = $("<p></p>").text('Running API test '+meta.name+':'+status);
  1175. $('#api_checks', checkDlg).append(test);
  1176. }); // update tests results
  1177. }); // runAPITests
  1178. /*
  1179. [].forEach.call(CONFIG, function(profile) {
  1180. alert("profile is:"+profile);
  1181. [].forEach.call(CONFIG[profile].tests, function(test) {
  1182. //UI.alert(test.url);
  1183. Host.downloadPosting(test.url, function(downloaded) {
  1184. alert("downloaded:");
  1185. //if (test.title == downloaded.title) alert("titles match:"+test.title);
  1186. }); //downloadPosting
  1187. }); //forEach test
  1188. }); //forEach profile
  1189. */
  1190. //$('#accordion',checkDlg).accordion();
  1191. checkDlg.dialog({
  1192. width: 700,
  1193. height: 500,
  1194. open: function () {
  1195. // http://stackoverflow.com/questions/2929487/putting-a-jquery-ui-accordion-in-a-jquery-ui-dialog
  1196. $('#accordion').accordion({
  1197. autoHeight: true
  1198. });
  1199. }
  1200. }); // show dialog
  1201. } // selfCheckDialog
  1202.  
  1203.  
  1204. // show a simple configuration dialog (WIP)
  1205. function setupDialog() {
  1206. //alert("configuration dialog is not yet implemented");
  1207. var checked = (Host.get_persistent('debug_mode_enabled', false) === true) ? 'checked' : '';
  1208. //dbLog("value is:"+get_persistent("debug_mode_enabled"));
  1209. //dbLog("persistent debug flag is:"+checked);
  1210. 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>');
  1211. setupDiv.click(function () {
  1212. //alert("changing persistent debug state");
  1213. Host.set_persistent('debug_mode_enabled', $('#debugcb').is(':checked'));
  1214. });
  1215. //MediaWiki editing stub, based on: https://www.mediawiki.org/wiki/API:Edit#Editing_via_Ajax
  1216. //only added here to show some status info in the setup dialog
  1217. Host.download('http://wiki.flightgear.org/api.php?action=query&prop=info|revisions&intoken=edit&rvprop=timestamp&titles=Main%20Page', function (response) {
  1218. var message = 'FlightGear wiki login status (AJAX):';
  1219. var status = response.statusText;
  1220. var color = (status == 'OK') ? 'green' : 'red';
  1221. Host.dbLog(message + status);
  1222. var statusDiv = $('<p>' + message + status + '</p>').css('color', color);
  1223. setupDiv.append(statusDiv);
  1224. });
  1225. setupDiv.dialog();
  1226. } // setupDialog
  1227.  
  1228.  
  1229. // this can be used to download/cache $FG_ROOT/options.xml so that fgfs CLI arguments can be recognized and post-processed automatically
  1230. // which can help transforming postings correctly
  1231. function downloadOptionsXML() {
  1232.  
  1233. // download $FG_ROOT/options.xml
  1234. Host.download("https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/options.xml?format=raw", function(response) {
  1235. var xml = response.responseText;
  1236. var doc = Host.make_doc(xml, 'text/xml');
  1237. // https://developer.mozilla.org/en-US/docs/Web/API/XPathResult
  1238. var options = Host.eval_xpath(doc, '//*/option', XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
  1239. // http://help.dottoro.com/ljgnejkp.php
  1240. Host.dbLog("Number of options found in options.xml:"+options.snapshotLength);
  1241. // http://help.dottoro.com/ljtfvvpx.php
  1242. // https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/options.xml
  1243. }); // end of options.xml download
  1244.  
  1245. } // downloadOptionsXML
  1246.  
  1247. function getProfile(url=undefined) {
  1248. if(url === undefined)
  1249. url=window.location.href;
  1250. else
  1251. url=url;
  1252. Host.dbLog("getProfile call URL is:"+url);
  1253. for (var profile in CONFIG) {
  1254. if (url.match(CONFIG[profile].url_reg) !== null) {
  1255. Host.dbLog('Matching website profile found');
  1256. var invocations = Host.get_persistent(Host.getScriptVersion(), 0);
  1257. Host.dbLog('Number of script invocations for version ' + Host.getScriptVersion() + ' is:' + invocations);
  1258.  
  1259. // determine if we want to show a config dialog
  1260. if (invocations === 0) {
  1261. Host.dbLog("ask for config dialog to be shown");
  1262. var response = UI.confirm('This is your first time running version ' + Host.getScriptVersion() + '\nConfigure now?');
  1263. if (response) {
  1264. // show configuration dialog (jQuery)
  1265. setupDialog();
  1266. }
  1267. else {
  1268. } // don't configure
  1269.  
  1270. }
  1271. // 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)
  1272. // FIXME: this is triggered/incremented by each click ...
  1273. Host.dbLog("increment number of script invocations");
  1274. Host.set_persistent(Host.getScriptVersion(), invocations + 1);
  1275. return CONFIG[profile];
  1276. } // matched website profile
  1277. Host.dbLog('Could not find matching URL in getProfile() call!');
  1278. } // for each profile
  1279. }// Get the HTML code that is selected
  1280.  
  1281. function getSelectedHtml() {
  1282. // From http://stackoverflow.com/a/6668159
  1283. var html = '',
  1284. selection = document.getSelection();
  1285. if (selection.rangeCount) {
  1286. var container = document.createElement('div');
  1287. for (var i = 0; i < selection.rangeCount; i++) {
  1288. container.appendChild(selection.getRangeAt(i).cloneContents());
  1289. }
  1290. html = container.innerHTML;
  1291. }
  1292. Host.dbLog('instantCquote(): Unprocessed HTML\n\'' + html + '\'');
  1293. return html;
  1294. }// Gets the selected text
  1295.  
  1296. function getSelectedText() {
  1297. return document.getSelection().toString();
  1298. }// Get the ID of the post
  1299. // (this needs some work so that it can be used by the AJAX mode, without an actual selection)
  1300.  
  1301. function getPostId(selection, profile, focus) {
  1302. if (focus !== undefined) {
  1303. Host.dbLog("Trying to get PostId with defined focus");
  1304. selection = selection.focusNode.parentNode;
  1305. } else {
  1306. Host.dbLog("Trying to get PostId with undefined focus");
  1307. selection = selection.anchorNode.parentNode;
  1308. }
  1309. while (selection.id.match(profile.content.idStyle) === null) {
  1310. selection = selection.parentNode;
  1311. }
  1312. Host.dbLog("Selection id is:"+selection.id);
  1313. return selection.id;
  1314. }
  1315.  
  1316. // Checks that the selection is valid
  1317. function checkValid(selection, profile) {
  1318. var ret = true,
  1319. selection_cp = {
  1320. },
  1321. tags = profile.content.parentTag;
  1322. for (var n = 0; n < 2; n++) {
  1323. if (n === 0) {
  1324. selection_cp = selection.anchorNode.parentNode;
  1325. } else {
  1326. selection_cp = selection.focusNode.parentNode;
  1327. }
  1328. while (true) {
  1329. if (selection_cp.tagName === 'BODY') {
  1330. ret = false;
  1331. break;
  1332. } else {
  1333. var cont = false;
  1334. for (var i = 0; i < tags.length; i++) {
  1335. if (selection_cp[tags[0]] === tags[i]) {
  1336. cont = true;
  1337. break;
  1338. }
  1339. }
  1340. if (cont) {
  1341. break;
  1342. } else {
  1343. selection_cp = selection_cp.parentNode;
  1344. }
  1345. }
  1346. }
  1347. }
  1348. ret = ret && (getPostId(selection, profile) === getPostId(selection, profile, 1));
  1349. return ret;
  1350. }// Extracts the raw text from a certain place, using an XPath
  1351.  
  1352. function extractFieldInfo(profile, id, field) {
  1353. if (field === 'content') {
  1354. Host.dbLog("Returning content (selection)");
  1355. return profile[field].selection();
  1356. } else {
  1357. Host.dbLog("Extracting field via xpath:"+field);
  1358. var xpath = '//*[@id="' + id + '"]/' + profile[field].xpath;
  1359. return Host.eval_xpath(document, xpath).stringValue; // document.evaluate(xpath, document, null, XPathResult.STRING_TYPE, null).stringValue;
  1360. }
  1361. }// Change the text using specified transformations
  1362.  
  1363. function applyTransformations(fieldInfo, trans) {
  1364. for (var i = 0; i < trans.length; i++) {
  1365. fieldInfo = trans[i](fieldInfo);
  1366. Host.dbLog('applyTransformations(): Multiple transformation, transformation after loop #' + (i + 1) + ':\n\'' + fieldInfo + '\'');
  1367. }
  1368. return fieldInfo;
  1369. } //applyTransformations
  1370.  
  1371. // Formats the quote
  1372.  
  1373. function createCquote(data, indirect_speech=false) {
  1374. if(!indirect_speech)
  1375. return nonQuotedRef(data); // conventional/verbatim selection
  1376. else {
  1377. // pattern match the content using a vector of regexes
  1378. data.content = transformSpeech(data.content, data.author, null, speechTransformations );
  1379. return nonQuotedRef(data);
  1380. }
  1381. }
  1382.  
  1383. function nonQuotedRef(data) { //TODO: rename
  1384. var template = Host.getTemplate();
  1385. var substituted = template
  1386. .replace('$CONTENT', data.content)
  1387. .replace('$URL',data.url)
  1388. .replace('$TITLE',data.title)
  1389. .replace('$AUTHOR',data.author)
  1390. .replace('$DATE',datef(data.date))
  1391. .replace('$ADDED',datef(data.date))
  1392. .replace('$SCRIPT_VERSION', Host.getScriptVersion() );
  1393. return substituted;
  1394. }//
  1395.  
  1396. // Output the text.
  1397. // Tries the jQuery dialog, and falls back to window.prompt()
  1398.  
  1399. function outputText(msg, original) {
  1400. try {
  1401. OUTPUT.jQueryTabbed(msg, original);
  1402. }
  1403. catch (err) {
  1404. msg = msg.replace(/&lt;\/syntaxhighligh(.)>/g, '</syntaxhighligh$1');
  1405. OUTPUT.msgbox(msg);
  1406. }
  1407. }
  1408.  
  1409. // #############
  1410. // # Utilities #
  1411. // #############
  1412.  
  1413. function extract(regex) {
  1414. return function (text) {
  1415. return text.match(regex) [1];
  1416. };
  1417. }
  1418. function prepend(prefix) {
  1419. return function (text) {
  1420. return prefix + text;
  1421. };
  1422. }
  1423. function removeComments(html) {
  1424. return html.replace(/<!--.*?-->/g, '');
  1425. }// Not currently used (as of June 2015), but kept just in case
  1426.  
  1427.  
  1428. // currently unused
  1429. function escapePipes(html) {
  1430. html = html.replace(/\|\|/g, '{{!!}n}');
  1431. html = html.replace(/\|\-/g, '{{!-}}');
  1432. return html.replace(/\|/g, '{{!}}');
  1433. }// Converts HTML <a href="...">...</a> tags to wiki links, internal if possible.
  1434.  
  1435. function a2wikilink(html) {
  1436. // Links to wiki images, because
  1437. // they need special treatment, or else they get displayed.
  1438. html = html.replace(/<a.*?href="http:\/\/wiki\.flightgear\.org\/File:(.*?)".*?>(.*?)<\/a>/g, '[[Media:$1|$2]]');
  1439. // Wiki links without custom text.
  1440. html = html.replace(/<a.*?href="http:\/\/wiki\.flightgear\.org\/(.*?)".*?>http:\/\/wiki\.flightgear\.org\/.*?<\/a>/g, '[[$1]]');
  1441. // Links to the wiki with custom text
  1442. html = html.replace(/<a.*?href="http:\/\/wiki\.flightgear\.org\/(.*?)".*?>(.*?)<\/a>/g, '[[$1|$2]]');
  1443. // Remove underscores from all wiki links
  1444. var list = html.match(/\[\[.*?\]\]/g);
  1445. if (list !== null) {
  1446. for (var i = 0; i < list.length; i++) {
  1447. html = html.replace(list[i], underscore2Space(list[i]));
  1448. }
  1449. } // Convert non-wiki links
  1450. // 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)
  1451.  
  1452. html = html.replace(/<a.*?href="(.*?)".*?>(.*?)<\/a>/g, '[$1 $2]');
  1453. // Remove triple dots from external links.
  1454. // Replace with raw URL (MediaWiki converts it to a link).
  1455. list = html.match(/\[.*?(\.\.\.).*?\]/g);
  1456. if (list !== null) {
  1457. for (var i = 0; i < list.length; i++) {
  1458. html = html.replace(list[i], list[i].match(/\[(.*?) .*?\]/) [1]);
  1459. }
  1460. }
  1461. return html;
  1462. }// Converts images, including images in <a> links
  1463.  
  1464. function img2link(html) {
  1465. html = html.replace(/<a[^<]*?href="([^<]*?)"[^<]*?><img.*?src="http:\/\/wiki\.flightgear\.org\/images\/.*?\/.*?\/(.*?)".*?><\/a>/g, '[[File:$2|250px|link=$1]]');
  1466. html = html.replace(/<img.*?src="http:\/\/wiki\.flightgear\.org\/images\/.*?\/.*?\/(.*?)".*?>/g, '[[File:$1|250px]]');
  1467. html = html.replace(/<a[^<]*?href="([^<]*?)"[^<]*?><img.*?src="(.*?)".*?><\/a>/g, '(see [$2 image], links to [$1 here])');
  1468. return html.replace(/<img.*?src="(.*?)".*?>/g, '(see the [$1 linked image])');
  1469. }// Converts smilies
  1470.  
  1471. function forum_smilies2text(html) {
  1472. html = html.replace(/<img src="\.\/images\/smilies\/icon_.*?\.gif" alt="(.*?)".*?>/g, '$1');
  1473. for (var i = 0; i < EMOTICONS.length; i++) {
  1474. html = html.replace(EMOTICONS[i][0], EMOTICONS[i][1]);
  1475. }
  1476. return html;
  1477. }// Converts font formatting
  1478.  
  1479. function forum_fontstyle2wikistyle(html) {
  1480. html = html.replace(/<span style="font-weight: bold">(.*?)<\/span>/g, '\'\'\'$1\'\'\'');
  1481. html = html.replace(/<span style="text-decoration: underline">(.*?)<\/span>/g, '<u>$1</u>');
  1482. html = html.replace(/<span style="font-style: italic">(.*?)<\/span>/g, '\'\'$1\'\'');
  1483. return html.replace(/<span class="posthilit">(.*?)<\/span>/g, '$1');
  1484. }// Converts code blocks
  1485.  
  1486. function forum_code2syntaxhighlight(html) {
  1487. var list = html.match(/<dl class="codebox">.*?<code>(.*?)<\/code>.*?<\/dl>/g),
  1488. data = [
  1489. ];
  1490. if (list === null) return html;
  1491. for (var n = 0; n < list.length; n++) {
  1492. data = html.match(/<dl class="codebox">.*?<code>(.*?)<\/code>.*?<\/dl>/);
  1493. html = html.replace(data[0], processCode(data));
  1494. }
  1495. return html;
  1496. }// Strips any whitespace from the beginning and end of a string
  1497.  
  1498. function stripWhitespace(html) {
  1499. html = html.replace(/^\s*?(\S)/, '$1');
  1500. return html.replace(/(\S)\s*?\z/, '$1');
  1501. }// Process code, including basic detection of language
  1502.  
  1503. function processCode(data) {
  1504. var lang = '',
  1505. code = data[1];
  1506. code = code.replace(/&nbsp;/g, ' ');
  1507. if (code.match(/=?.*?\(?.*?\)?;/) !== null) lang = 'nasal';
  1508. if (code.match(/&lt;.*?&gt;.*?&lt;\/.*?&gt;/) !== null || code.match(/&lt;!--.*?--&gt;/) !== null) lang = 'xml';
  1509. code = code.replace(/<br\/?>/g, '\n');
  1510. return '<syntaxhighlight lang="' + lang + '" enclose="div">\n' + code + '\n&lt;/syntaxhighlight>';
  1511. }// Converts quote blocks to Cquotes
  1512.  
  1513. function forum_quote2cquote(html) {
  1514. html = html.replace(/<blockquote class="uncited"><div>(.*?)<\/div><\/blockquote>/g, '{{cquote|$1}}');
  1515. if (html.match(/<blockquote>/g) === null) return html;
  1516. var numQuotes = html.match(/<blockquote>/g).length;
  1517. for (var n = 0; n < numQuotes; n++) {
  1518. html = html.replace(/<blockquote><div><cite>(.*?) wrote.*?:<\/cite>(.*?)<\/div><\/blockquote>/, '{{cquote|$2|$1}}');
  1519. }
  1520. return html;
  1521. }// Converts videos to wiki style
  1522.  
  1523. function vid2wiki(html) {
  1524. // YouTube
  1525. 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}}');
  1526. // Vimeo
  1527. html = html.replace(/<iframe src="http:\/\/player\.vimeo\.com\/video\/(.*?)\?.*?" width="(.*?)" height="(.*?)".*?>.*?<\/iframe>/g, '{{#ev:vimeo|$1|$2x$3}}');
  1528. return html.replace(/\[.*? Watch on Vimeo\]/g, '');
  1529. }// Not currently used (as of June 2015), but kept just in case
  1530.  
  1531. // currently unused
  1532. function escapeEquals(html) {
  1533. return html.replace(/=/g, '{{=}}');
  1534. }// <br> to newline.
  1535.  
  1536. function forum_br2newline(html) {
  1537. html = html.replace(/<br\/?><br\/?>/g, '\n');
  1538. return html.replace(/<br\/?>/g, '\n\n');
  1539. }// Forum list to wiki style
  1540.  
  1541. function list2wiki(html) {
  1542. var list = html.match(/<ul>(.*?)<\/ul>/g);
  1543. if (list !== null) {
  1544. for (var i = 0; i < list.length; i++) {
  1545. html = html.replace(/<li>(.*?)<\/li>/g, '* $1\n');
  1546. }
  1547. }
  1548. list = html.match(/<ol.*?>(.*?)<\/ol>/g);
  1549. if (list !== null) {
  1550. for (var i = 0; i < list.length; i++) {
  1551. html = html.replace(/<li>(.*?)<\/li>/g, '# $1\n');
  1552. }
  1553. }
  1554. html = html.replace(/<\/?[uo]l>/g, '');
  1555. return html;
  1556. }
  1557. function nowiki(text) {
  1558. return '<nowiki>' + text + '</nowiki>';
  1559. }// Returns the correct ordinal adjective
  1560.  
  1561. function ordAdj(date) {
  1562. date = date.toString();
  1563. if (date == '11' || date == '12' || date == '13') {
  1564. return 'th';
  1565. } else if (date.substr(1) == '1' || date == '1') {
  1566. return 'st';
  1567. } else if (date.substr(1) == '2' || date == '2') {
  1568. return 'nd';
  1569. } else if (date.substr(1) == '3' || date == '3') {
  1570. return 'rd';
  1571. } else {
  1572. return 'th';
  1573. }
  1574. }
  1575.  
  1576. // Formats the date to this format: Apr 26th, 2015
  1577. function datef(text) {
  1578. var date = new Date(text);
  1579. return MONTHS[date.getMonth()] + ' ' + date.getDate() + ordAdj(date.getDate()) + ', ' + date.getFullYear();
  1580. }
  1581. function underscore2Space(str) {
  1582. return str.replace(/_/g, ' ');
  1583. }
  1584.  
  1585. // IGNORE EVERYTHING THAT FOLLOWS:
  1586. // This is an experiment to use GA/GP (genetic programming) to help procedurally evolve xpath and regex expressions if/when the underlying websites change
  1587. // so that we don't have to manually update/edit the script accordingly (this would also work for mobile themes etc)
  1588. // For now, this is heavily based on the genetic.js framework/examples: http://subprotocol.com/system/genetic-hello-world.html
  1589. // The idea is to evolve the xpath/regex expression by evaluating its return value against the expected/desired value
  1590. // the most important thing here is having a suitable fitness function
  1591. //
  1592.  
  1593.  
  1594.  
  1595. function evolve_expression_test() {
  1596. try {
  1597. var genetic = Genetic.create();
  1598.  
  1599. // TODO: use minimizer: redundant_bytes + duration_msec + xpath.length
  1600. genetic.optimize = Genetic.Optimize.Maximize;
  1601. genetic.select1 = Genetic.Select1.Tournament2;
  1602. genetic.select2 = Genetic.Select2.Tournament2;
  1603. genetic.seed = function() {
  1604.  
  1605. function randomString(len) {
  1606. var text = "";
  1607. var charset = "\\abcdefghijklmnopqrstuvwxyz0123456789[] ()<>*.,";
  1608. for(var i=0;i<len;i++)
  1609. text += charset.charAt(Math.floor(Math.random() * charset.length));
  1610. return text;
  1611. }
  1612. // create random strings that are equal in length to solution
  1613. return randomString( this.userData["solution"].length);
  1614. };
  1615.  
  1616. genetic.mutate = function(entity) {
  1617. function replaceAt(str, index, character) {
  1618. return str.substr(0, index) + character + str.substr(index+character.length);
  1619. }
  1620. // chromosomal drift
  1621. var i = Math.floor(Math.random()*entity.length);
  1622. return replaceAt(entity, i, String.fromCharCode(entity.charCodeAt(i) + (Math.floor(Math.random()*2) ? 1 : -1)));
  1623. };
  1624.  
  1625. genetic.crossover = function(mother, father) {
  1626.  
  1627. // two-point crossover
  1628. var len = mother.length;
  1629. var ca = Math.floor(Math.random()*len);
  1630. var cb = Math.floor(Math.random()*len);
  1631. if (ca > cb) {
  1632. var tmp = cb;
  1633. cb = ca;
  1634. ca = tmp;
  1635. }
  1636. var son = father.substr(0,ca) + mother.substr(ca, cb-ca) + father.substr(cb);
  1637. var daughter = mother.substr(0,ca) + father.substr(ca, cb-ca) + mother.substr(cb);
  1638. return [son, daughter];
  1639. };
  1640. genetic.determineExcessBytes = function (text, needle) {
  1641. return text.length - needle.length;
  1642. };
  1643. genetic.containsText = function (text, needle) {
  1644. return text.search(needle);
  1645. };
  1646. genetic.isValid = function(exp) {
  1647.  
  1648. };
  1649. /* myFitness:
  1650. * - must be a valid xpath/regex expression (try/call)
  1651. * - must containsText the needle
  1652. * - low relative offset in text (begin/end)
  1653. * - excessBytes
  1654. * - short expression (expression length)
  1655. * - expression footprint (runtime)
  1656. */
  1657.  
  1658. // TODO: the fitness function should validate each xpath/regex first
  1659. genetic.fitness = function(entity) {
  1660. var fitness = 0;
  1661. var result;
  1662. var validExp = 0.1;
  1663. var hasToken = 0.1;
  1664. var t = this.userData.tests[0].haystack;
  1665. //var regex = new RegExp(this.userData.solution);
  1666. //var output = t.match( new RegExp("From: (.*) <.*@.*>"))[1];
  1667. // TODO: use search & match for improving the fitness
  1668. if (0)
  1669. try {
  1670. var regex = new RegExp(entity);
  1671. var output = t.search( regex);
  1672. validExp = 10;
  1673. }
  1674. catch(e) {
  1675. validExp = 2;
  1676. }
  1677. var i;
  1678. for (i=0;i<entity.length;++i) {
  1679. // increase fitness for each character that matches
  1680. if (entity[i] == this.userData["solution"][i])
  1681. fitness += 1;
  1682. // award fractions of a point as we get warmer
  1683. fitness += (127-Math.abs(entity.charCodeAt(i) - this.userData["solution"].charCodeAt(i)))/50;
  1684. }
  1685.  
  1686. return fitness; // + (1*validExp + 1* hasToken);
  1687. };
  1688.  
  1689. genetic.generation = function(pop, generation, stats) {
  1690. // stop running once we've reached the solution
  1691. return pop[0].entity != this.userData["solution"];
  1692. };
  1693.  
  1694. genetic.notification = function(pop, generation, stats, isFinished) {
  1695.  
  1696. function lerp(a, b, p) {
  1697. return a + (b-a)*p;
  1698. }
  1699. var value = pop[0].entity;
  1700. this.last = this.last||value;
  1701. if (pop != 0 && value == this.last)
  1702. return;
  1703. var solution = [];
  1704. var i;
  1705. for (i=0;i<value.length;++i) {
  1706. var diff = value.charCodeAt(i) - this.last.charCodeAt(i);
  1707. var style = "background: transparent;";
  1708. if (diff > 0) {
  1709. style = "background: rgb(0,200,50); color: #fff;";
  1710. } else if (diff < 0) {
  1711. style = "background: rgb(0,100,50); color: #fff;";
  1712. }
  1713.  
  1714. solution.push("<span style=\"" + style + "\">" + value[i] + "</span>");
  1715. }
  1716. var t = this.userData.tests[0].haystack;
  1717. //console.log("haystack is:"+t);
  1718. // "From: John Doe <John@do...> - 2020-07-02 17:36:03", needle: "John Doe"}, /From: (.*) <.*@.*>/
  1719. var regex = new RegExp(this.userData.solution);
  1720. //var output = t.match( new RegExp("From: (.*) <.*@.*>"))[1];
  1721. // TODO: use search & match for improving the fitness
  1722. var output = t.search( new RegExp(value));
  1723. var buf = "";
  1724. buf += "<tr>";
  1725. buf += "<td>" + generation + "</td>";
  1726. buf += "<td>" + pop[0].fitness.toPrecision(5) + "</td>";
  1727. buf += "<td>" + solution.join("") + "</td>";
  1728. buf += "<td>" + output + "</td>";
  1729. buf += "</tr>";
  1730. $("#results tbody").prepend(buf);
  1731. this.last = value;
  1732. };
  1733. /*
  1734. genetic.notification2 = function(pop, generation, stats, isFinished) {
  1735.  
  1736. function lerp(a, b, p) {
  1737. return a + (b-a)*p;
  1738. }
  1739. var value = pop[0].entity;
  1740. this.last = this.last||value;
  1741. if (pop != 0 && value == this.last)
  1742. return;
  1743.  
  1744. var solution = [];
  1745. var i;
  1746. for (i=0;i<value.length;++i) {
  1747. solution.push(value[i]);
  1748. }
  1749. console.log("Generation:"+ generation + " Fitness:" + pop[0].fitness.toPrecision(5) + " Solution:" + solution.join(""));
  1750. this.last = value;
  1751. };
  1752. */
  1753. var config = {
  1754. "iterations": 4000
  1755. , "size": 250
  1756. , "crossover": 0.3
  1757. , "mutation": 0.4
  1758. , "skip": 30 // notifications
  1759. //, "webWorkers": false
  1760. };
  1761.  
  1762.  
  1763. /*
  1764. var profile = CONFIG['Sourceforge Mailing list'];
  1765. var posting = profile.tests[0];
  1766. var author_xpath = profile.title.xpath;
  1767. */
  1768.  
  1769. var regexTests = [
  1770. {haystack: "From: John Doe <John@do...> - 2020-07-02 17:36:03", needle: "John Doe"},
  1771. {haystack: "From: Marc Twain <Marc@ta...> - 2010-01-03 07:36:03", needle: "Marc Twain"},
  1772. {haystack: "From: George W. Bush <GWB@wh...> - 2055-11-11 17:33:13", needle: "George W. Bush"}
  1773. ];
  1774. // the regex we want to evolve
  1775. var solution = "From: (.*) <.*@.*>";
  1776.  
  1777. // let's assume, we'd like to evolve a regex expression like this one
  1778. var userData = {
  1779. solution: solution,
  1780. tests: regexTests
  1781. };
  1782. genetic.evolve(config, userData);
  1783.  
  1784. //console.log("genetic.js is loaded and working, but disabled for now");
  1785. } // try
  1786. catch (e) {
  1787. console.log("genetic.js error:\n" +e.message);
  1788. } // catch
  1789. } // evolveExpression_test()
  1790.  
  1791.  
  1792. if(0) //TODO: expose via development tab
  1793. try {
  1794. // https://github.com/cazala/synaptic
  1795. var Neuron = synaptic.Neuron,
  1796. Layer = synaptic.Layer,
  1797. Network = synaptic.Network,
  1798. Trainer = synaptic.Trainer,
  1799. Architect = synaptic.Architect;
  1800. function Perceptron(input, hidden, output)
  1801. {
  1802. // create the layers
  1803. var inputLayer = new Layer(input);
  1804. var hiddenLayer = new Layer(hidden);
  1805. var outputLayer = new Layer(output);
  1806.  
  1807. // connect the layers
  1808. inputLayer.project(hiddenLayer);
  1809. hiddenLayer.project(outputLayer);
  1810.  
  1811. // set the layers
  1812. this.set({
  1813. input: inputLayer,
  1814. hidden: [hiddenLayer],
  1815. output: outputLayer
  1816. });
  1817. }
  1818.  
  1819. // extend the prototype chain
  1820. Perceptron.prototype = new Network();
  1821. Perceptron.prototype.constructor = Perceptron;
  1822. var myPerceptron = new Perceptron(2,3,1);
  1823. var myTrainer = new Trainer(myPerceptron);
  1824.  
  1825. myTrainer.XOR(); // { error: 0.004998819355993572, iterations: 21871, time: 356 }
  1826.  
  1827. myPerceptron.activate([0,0]); // 0.0268581547421616
  1828. myPerceptron.activate([1,0]); // 0.9829673642853368
  1829. myPerceptron.activate([0,1]); // 0.9831714267395621
  1830. myPerceptron.activate([1,1]); // 0.02128894618097928
  1831. console.log("Syntaptic loaded");
  1832. } catch(e) {
  1833. UI.alert(e.message);
  1834. }