atd.core.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. /*
  2. * atd.core.js - A building block to create a front-end for AtD
  3. * Author : Raphael Mudge, Automattic
  4. * License : LGPL
  5. * Project : http://www.afterthedeadline.com/developers.slp
  6. * Contact : raffi@automattic.com
  7. */
  8. /* jshint sub: true, devel: true, onevar: false, smarttabs: true, loopfunc: true */
  9. /* exported EXPORTED_SYMBOLS, atd_sprintf */
  10. /* EXPORTED_SYMBOLS is set so this file can be a JavaScript Module */
  11. var EXPORTED_SYMBOLS = ['AtDCore'];
  12. function AtDCore() {
  13. /* these are the categories of errors AtD should ignore */
  14. this.ignore_types = ['Bias Language', 'Cliches', 'Complex Expression', 'Diacritical Marks', 'Double Negatives', 'Hidden Verbs', 'Jargon Language', 'Passive voice', 'Phrases to Avoid', 'Redundant Expression'];
  15. /* these are the phrases AtD should ignore */
  16. this.ignore_strings = {};
  17. /* Localized strings */
  18. // Back-compat, not used
  19. this.i18n = {};
  20. }
  21. /*
  22. * Internationalization Functions
  23. */
  24. AtDCore.prototype.getLang = function( key, defaultk ) {
  25. return ( window.AtD_l10n_r0ar && window.AtD_l10n_r0ar[key] ) || defaultk;
  26. };
  27. AtDCore.prototype.addI18n = function( obj ) {
  28. // Back-compat
  29. window.AtD_l10n_r0ar = obj;
  30. };
  31. /*
  32. * Setters
  33. */
  34. AtDCore.prototype.setIgnoreStrings = function(string) {
  35. var parent = this;
  36. this.map(string.split(/,\s*/g), function(string) {
  37. parent.ignore_strings[string] = 1;
  38. });
  39. };
  40. AtDCore.prototype.showTypes = function(string) {
  41. var show_types = string.split(/,\s*/g);
  42. var types = {};
  43. /* set some default types that we want to make optional */
  44. /* grammar checker options */
  45. types['Double Negatives'] = 1;
  46. types['Hidden Verbs'] = 1;
  47. types['Passive voice'] = 1;
  48. types['Bias Language'] = 1;
  49. /* style checker options */
  50. types['Cliches'] = 1;
  51. types['Complex Expression'] = 1;
  52. types['Diacritical Marks'] = 1;
  53. types['Jargon Language'] = 1;
  54. types['Phrases to Avoid'] = 1;
  55. types['Redundant Expression'] = 1;
  56. var ignore_types = [];
  57. this.map(show_types, function(string) {
  58. types[string] = undefined;
  59. });
  60. this.map(this.ignore_types, function(string) {
  61. if (types[string] !== undefined) {
  62. ignore_types.push(string);
  63. }
  64. });
  65. this.ignore_types = ignore_types;
  66. };
  67. /*
  68. * Error Parsing Code
  69. */
  70. AtDCore.prototype.makeError = function(error_s, tokens, type, seps/*, pre*/) {
  71. var struct = {};
  72. struct.type = type;
  73. struct.string = error_s;
  74. struct.tokens = tokens;
  75. if (new RegExp('\\b' + error_s + '\\b').test(error_s)) {
  76. struct.regexp = new RegExp('(?!'+error_s+'<)\\b' + error_s.replace(/\s+/g, seps) + '\\b');
  77. }
  78. else if (new RegExp(error_s + '\\b').test(error_s)) {
  79. struct.regexp = new RegExp('(?!'+error_s+'<)' + error_s.replace(/\s+/g, seps) + '\\b');
  80. }
  81. else if (new RegExp('\\b' + error_s).test(error_s)) {
  82. struct.regexp = new RegExp('(?!'+error_s+'<)\\b' + error_s.replace(/\s+/g, seps));
  83. }
  84. else {
  85. struct.regexp = new RegExp('(?!'+error_s+'<)' + error_s.replace(/\s+/g, seps));
  86. }
  87. struct.used = false; /* flag whether we've used this rule or not */
  88. return struct;
  89. };
  90. AtDCore.prototype.addToErrorStructure = function(errors, list, type, seps) {
  91. var parent = this;
  92. this.map(list, function(error) {
  93. var tokens = error['word'].split(/\s+/);
  94. var pre = error['pre'];
  95. var first = tokens[0];
  96. if (errors['__' + first] === undefined) {
  97. errors['__' + first] = {};
  98. errors['__' + first].pretoks = {};
  99. errors['__' + first].defaults = [];
  100. }
  101. if (pre === '') {
  102. errors['__' + first].defaults.push(parent.makeError(error['word'], tokens, type, seps, pre));
  103. } else {
  104. if (errors['__' + first].pretoks['__' + pre] === undefined) {
  105. errors['__' + first].pretoks['__' + pre] = [];
  106. }
  107. errors['__' + first].pretoks['__' + pre].push(parent.makeError(error['word'], tokens, type, seps, pre));
  108. }
  109. });
  110. };
  111. AtDCore.prototype.buildErrorStructure = function(spellingList, enrichmentList, grammarList) {
  112. var seps = this._getSeparators();
  113. var errors = {};
  114. this.addToErrorStructure(errors, spellingList, 'hiddenSpellError', seps);
  115. this.addToErrorStructure(errors, grammarList, 'hiddenGrammarError', seps);
  116. this.addToErrorStructure(errors, enrichmentList, 'hiddenSuggestion', seps);
  117. return errors;
  118. };
  119. AtDCore.prototype._getSeparators = function() {
  120. var re = '', i;
  121. var str = '"s!#$%&()*+,./:;<=>?@[\\]^_{|}';
  122. // Build word separator regexp
  123. for (i=0; i<str.length; i++) {
  124. re += '\\' + str.charAt(i);
  125. }
  126. return '(?:(?:[\xa0' + re + '])|(?:\\-\\-))+';
  127. };
  128. AtDCore.prototype.processXML = function(responseXML) {
  129. /* types of errors to ignore */
  130. var types = {};
  131. this.map(this.ignore_types, function(type) {
  132. types[type] = 1;
  133. });
  134. /* save suggestions in the editor object */
  135. this.suggestions = [];
  136. /* process through the errors */
  137. var errors = responseXML.getElementsByTagName('error');
  138. /* words to mark */
  139. var grammarErrors = [];
  140. var spellingErrors = [];
  141. var enrichment = [];
  142. for (var i = 0; i < errors.length; i++) {
  143. if (errors[i].getElementsByTagName('string').item(0).firstChild !== null) {
  144. var errorString = errors[i].getElementsByTagName('string').item(0).firstChild.data;
  145. var errorType = errors[i].getElementsByTagName('type').item(0).firstChild.data;
  146. var errorDescription = errors[i].getElementsByTagName('description').item(0).firstChild.data;
  147. var errorContext;
  148. if (errors[i].getElementsByTagName('precontext').item(0).firstChild !== null) {
  149. errorContext = errors[i].getElementsByTagName('precontext').item(0).firstChild.data;
  150. } else {
  151. errorContext = '';
  152. }
  153. /* create a hashtable with information about the error in the editor object, we will use this later
  154. to populate a popup menu with information and suggestions about the error */
  155. if (this.ignore_strings[errorString] === undefined) {
  156. var suggestion = {};
  157. suggestion['description'] = errorDescription;
  158. suggestion['suggestions'] = [];
  159. /* used to find suggestions when a highlighted error is clicked on */
  160. suggestion['matcher'] = new RegExp('^' + errorString.replace(/\s+/, this._getSeparators()) + '$');
  161. suggestion['context'] = errorContext;
  162. suggestion['string'] = errorString;
  163. suggestion['type'] = errorType;
  164. this.suggestions.push(suggestion);
  165. if (errors[i].getElementsByTagName('suggestions').item(0) !== null) {
  166. var suggestions = errors[i].getElementsByTagName('suggestions').item(0).getElementsByTagName('option');
  167. for (var j = 0; j < suggestions.length; j++) {
  168. suggestion['suggestions'].push(suggestions[j].firstChild.data);
  169. }
  170. }
  171. /* setup the more info url */
  172. if (errors[i].getElementsByTagName('url').item(0) !== null) {
  173. var errorUrl = errors[i].getElementsByTagName('url').item(0).firstChild.data;
  174. suggestion['moreinfo'] = errorUrl + '&theme=tinymce';
  175. }
  176. if (types[errorDescription] === undefined) {
  177. if (errorType === 'suggestion') {
  178. enrichment.push({ word: errorString, pre: errorContext });
  179. }
  180. if (errorType === 'grammar') {
  181. grammarErrors.push({ word: errorString, pre: errorContext });
  182. }
  183. }
  184. if (errorType === 'spelling' || errorDescription === 'Homophone') {
  185. spellingErrors.push({ word: errorString, pre: errorContext });
  186. }
  187. if (errorDescription === 'Cliches') {
  188. suggestion['description'] = 'Clichés'; /* done here for backwards compatability with current user settings */
  189. }
  190. if (errorDescription === 'Spelling') {
  191. suggestion['description'] = this.getLang('menu_title_spelling', 'Spelling');
  192. }
  193. if (errorDescription === 'Repeated Word') {
  194. suggestion['description'] = this.getLang('menu_title_repeated_word', 'Repeated Word');
  195. }
  196. if (errorDescription === 'Did you mean...') {
  197. suggestion['description'] = this.getLang('menu_title_confused_word', 'Did you mean...');
  198. }
  199. } // end if ignore[errorString] == undefined
  200. } // end if
  201. } // end for loop
  202. var errorStruct;
  203. var ecount = spellingErrors.length + grammarErrors.length + enrichment.length;
  204. if (ecount > 0) {
  205. errorStruct = this.buildErrorStructure(spellingErrors, enrichment, grammarErrors);
  206. } else {
  207. errorStruct = undefined;
  208. }
  209. /* save some state in this object, for retrieving suggestions later */
  210. return { errors: errorStruct, count: ecount, suggestions: this.suggestions };
  211. };
  212. AtDCore.prototype.findSuggestion = function(element) {
  213. var text = element.innerHTML;
  214. var context = ( this.getAttrib(element, 'pre') + '' ).replace(/[\\,!\\?\\."\s]/g, '');
  215. if (this.getAttrib(element, 'pre') === undefined) {
  216. alert(element.innerHTML);
  217. }
  218. var errorDescription;
  219. var len = this.suggestions.length;
  220. for (var i = 0; i < len; i++) {
  221. if ((context === '' || context === this.suggestions[i]['context']) && this.suggestions[i]['matcher'].test(text)) {
  222. errorDescription = this.suggestions[i];
  223. break;
  224. }
  225. }
  226. return errorDescription;
  227. };
  228. /*
  229. * TokenIterator class
  230. */
  231. function TokenIterator(tokens) {
  232. this.tokens = tokens;
  233. this.index = 0;
  234. this.count = 0;
  235. this.last = 0;
  236. }
  237. TokenIterator.prototype.next = function() {
  238. var current = this.tokens[this.index];
  239. this.count = this.last;
  240. this.last += current.length + 1;
  241. this.index++;
  242. /* strip single quotes from token, AtD does this when presenting errors */
  243. if (current !== '') {
  244. if (current[0] === '\'') {
  245. current = current.substring(1, current.length);
  246. }
  247. if (current[current.length - 1] === '\'') {
  248. current = current.substring(0, current.length - 1);
  249. }
  250. }
  251. return current;
  252. };
  253. TokenIterator.prototype.hasNext = function() {
  254. return this.index < this.tokens.length;
  255. };
  256. TokenIterator.prototype.hasNextN = function(n) {
  257. return (this.index + n) < this.tokens.length;
  258. };
  259. TokenIterator.prototype.skip = function(m, n) {
  260. this.index += m;
  261. this.last += n;
  262. if (this.index < this.tokens.length) {
  263. this.count = this.last - this.tokens[this.index].length;
  264. }
  265. };
  266. TokenIterator.prototype.getCount = function() {
  267. return this.count;
  268. };
  269. TokenIterator.prototype.peek = function(n) {
  270. var peepers = [];
  271. var end = this.index + n;
  272. for (var x = this.index; x < end; x++) {
  273. peepers.push(this.tokens[x]);
  274. }
  275. return peepers;
  276. };
  277. /*
  278. * code to manage highlighting of errors
  279. */
  280. AtDCore.prototype.markMyWords = function(container_nodes, errors) {
  281. var seps = new RegExp(this._getSeparators()),
  282. nl = [],
  283. ecount = 0, /* track number of highlighted errors */
  284. parent = this,
  285. bogus = this._isTinyMCE ? ' data-mce-bogus="1"' : '',
  286. emptySpan = '<span class="mceItemHidden"' + bogus + '>&nbsp;</span>',
  287. textOnlyMode;
  288. /**
  289. * Split a text node into an ordered list of siblings:
  290. * - text node to the left of the match
  291. * - the element replacing the match
  292. * - text node to the right of the match
  293. *
  294. * We have to leave the text to the left and right of the match alone
  295. * in order to prevent XSS
  296. *
  297. * @return array
  298. */
  299. function splitTextNode( textnode, regexp, replacement ) {
  300. var text = textnode.nodeValue,
  301. index = text.search( regexp ),
  302. match = text.match( regexp ),
  303. captured = [],
  304. cursor;
  305. if ( index < 0 || ! match.length ) {
  306. return [ textnode ];
  307. }
  308. if ( index > 0 ) {
  309. // capture left text node
  310. captured.push( document.createTextNode( text.substr( 0, index ) ) );
  311. }
  312. // capture the replacement of the matched string
  313. captured.push( parent.create( match[0].replace( regexp, replacement ) ) );
  314. cursor = index + match[0].length;
  315. if ( cursor < text.length ) {
  316. // capture right text node
  317. captured.push( document.createTextNode( text.substr( cursor ) ) );
  318. }
  319. return captured;
  320. }
  321. function _isInPre( node ) {
  322. if ( node ) {
  323. while ( node.parentNode ) {
  324. if ( node.nodeName === 'PRE' ) {
  325. return true;
  326. }
  327. node = node.parentNode;
  328. }
  329. }
  330. return false;
  331. }
  332. /* Collect all text nodes */
  333. /* Our goal--ignore nodes that are already wrapped */
  334. this._walk( container_nodes, function( n ) {
  335. if ( n.nodeType === 3 && ! parent.isMarkedNode( n ) && ! _isInPre( n ) ) {
  336. nl.push( n );
  337. }
  338. });
  339. /* walk through the relevant nodes */
  340. var iterator;
  341. this.map( nl, function( n ) {
  342. var v;
  343. if ( n.nodeType === 3 ) {
  344. v = n.nodeValue; /* we don't want to mangle the HTML so use the actual encoded string */
  345. var tokens = n.nodeValue.split( seps ); /* split on the unencoded string so we get access to quotes as " */
  346. var previous = '';
  347. var doReplaces = [];
  348. iterator = new TokenIterator(tokens);
  349. while ( iterator.hasNext() ) {
  350. var token = iterator.next();
  351. var current = errors['__' + token];
  352. var defaults;
  353. if ( current !== undefined && current.pretoks !== undefined ) {
  354. defaults = current.defaults;
  355. current = current.pretoks['__' + previous];
  356. var done = false;
  357. var prev, curr;
  358. prev = v.substr(0, iterator.getCount());
  359. curr = v.substr(prev.length, v.length);
  360. var checkErrors = function( error ) {
  361. if ( error !== undefined && ! error.used && foundStrings[ '__' + error.string ] === undefined && error.regexp.test( curr ) ) {
  362. foundStrings[ '__' + error.string ] = 1;
  363. doReplaces.push([ error.regexp, '<span class="'+error.type+'" pre="'+previous+'"' + bogus + '>$&</span>' ]);
  364. error.used = true;
  365. done = true;
  366. }
  367. }; // jshint ignore:line
  368. var foundStrings = {};
  369. if (current !== undefined) {
  370. previous = previous + ' ';
  371. parent.map(current, checkErrors);
  372. }
  373. if (!done) {
  374. previous = '';
  375. parent.map(defaults, checkErrors);
  376. }
  377. }
  378. previous = token;
  379. } // end while
  380. /* do the actual replacements on this span */
  381. if ( doReplaces.length > 0 ) {
  382. var newNode = n;
  383. for ( var x = 0; x < doReplaces.length; x++ ) {
  384. var regexp = doReplaces[x][0], result = doReplaces[x][1];
  385. /* it's assumed that this function is only being called on text nodes (nodeType == 3), the iterating is necessary
  386. because eventually the whole thing gets wrapped in an mceItemHidden span and from there it's necessary to
  387. handle each node individually. */
  388. var bringTheHurt = function( node ) {
  389. var span, splitNodes;
  390. if ( node.nodeType === 3 ) {
  391. ecount++;
  392. /* sometimes IE likes to ignore the space between two spans, solution is to insert a placeholder span with
  393. a non-breaking space. The markup removal code substitutes this span for a space later */
  394. if ( parent.isIE() && node.nodeValue.length > 0 && node.nodeValue.substr(0, 1) === ' ' ) {
  395. return parent.create( emptySpan + node.nodeValue.substr( 1, node.nodeValue.length - 1 ).replace( regexp, result ), false );
  396. } else {
  397. if ( textOnlyMode ) {
  398. return parent.create( node.nodeValue.replace( regexp, result ), false );
  399. }
  400. span = parent.create( '<span />' );
  401. if ( typeof textOnlyMode === 'undefined' ) {
  402. // cache this to avoid adding / removing nodes unnecessarily
  403. textOnlyMode = typeof span.appendChild !== 'function';
  404. if ( textOnlyMode ) {
  405. parent.remove( span );
  406. return parent.create( node.nodeValue.replace( regexp, result ), false );
  407. }
  408. }
  409. // "Visual" mode
  410. splitNodes = splitTextNode( node, regexp, result );
  411. for ( var i = 0; i < splitNodes.length; i++ ) {
  412. span.appendChild( splitNodes[i] );
  413. }
  414. node = span;
  415. return node;
  416. }
  417. }
  418. else {
  419. var contents = parent.contents(node);
  420. for ( var y = 0; y < contents.length; y++ ) {
  421. if ( contents[y].nodeType === 3 && regexp.test( contents[y].nodeValue ) ) {
  422. var nnode;
  423. if ( parent.isIE() && contents[y].nodeValue.length > 0 && contents[y].nodeValue.substr(0, 1) === ' ') {
  424. nnode = parent.create( emptySpan + contents[y].nodeValue.substr( 1, contents[y].nodeValue.length - 1 ).replace( regexp, result ), true );
  425. } else {
  426. nnode = parent.create( contents[y].nodeValue.replace( regexp, result ), true );
  427. }
  428. parent.replaceWith( contents[y], nnode );
  429. parent.removeParent( nnode );
  430. ecount++;
  431. return node; /* we did a replacement so we can call it quits, errors only get used once */
  432. }
  433. }
  434. return node;
  435. }
  436. }; // jshint ignore:line
  437. newNode = bringTheHurt(newNode);
  438. }
  439. parent.replaceWith(n, newNode);
  440. }
  441. }
  442. });
  443. return ecount;
  444. };
  445. AtDCore.prototype._walk = function(elements, f) {
  446. var i;
  447. for (i = 0; i < elements.length; i++) {
  448. f.call(f, elements[i]);
  449. this._walk(this.contents(elements[i]), f);
  450. }
  451. };
  452. AtDCore.prototype.removeWords = function(node, w) {
  453. var count = 0;
  454. var parent = this;
  455. this.map(this.findSpans(node).reverse(), function(n) {
  456. if (n && (parent.isMarkedNode(n) || parent.hasClass(n, 'mceItemHidden') || parent.isEmptySpan(n)) ) {
  457. if (n.innerHTML === '&nbsp;') {
  458. var nnode = document.createTextNode(' '); /* hax0r */
  459. parent.replaceWith(n, nnode);
  460. } else if (!w || n.innerHTML === w) {
  461. parent.removeParent(n);
  462. count++;
  463. }
  464. }
  465. });
  466. return count;
  467. };
  468. AtDCore.prototype.isEmptySpan = function(node) {
  469. return (this.getAttrib(node, 'class') === '' && this.getAttrib(node, 'style') === '' && this.getAttrib(node, 'id') === '' && !this.hasClass(node, 'Apple-style-span') && this.getAttrib(node, 'mce_name') === '');
  470. };
  471. AtDCore.prototype.isMarkedNode = function(node) {
  472. return (this.hasClass(node, 'hiddenGrammarError') || this.hasClass(node, 'hiddenSpellError') || this.hasClass(node, 'hiddenSuggestion'));
  473. };
  474. /*
  475. * Context Menu Helpers
  476. */
  477. AtDCore.prototype.applySuggestion = function(element, suggestion) {
  478. if (suggestion === '(omit)') {
  479. this.remove(element);
  480. }
  481. else {
  482. var node = this.create(suggestion);
  483. this.replaceWith(element, node);
  484. this.removeParent(node);
  485. }
  486. };
  487. /*
  488. * Check for an error
  489. */
  490. AtDCore.prototype.hasErrorMessage = function(xmlr) {
  491. return (xmlr !== undefined && xmlr.getElementsByTagName('message').item(0) !== null);
  492. };
  493. AtDCore.prototype.getErrorMessage = function(xmlr) {
  494. return xmlr.getElementsByTagName('message').item(0);
  495. };
  496. /* this should always be an error, alas... not practical */
  497. AtDCore.prototype.isIE = function() {
  498. return navigator.appName === 'Microsoft Internet Explorer';
  499. };
  500. // TODO: this doesn't seem used anywhere in AtD, moved here from install_atd_l10n.js for eventual back-compat
  501. /* a quick poor man's sprintf */
  502. function atd_sprintf(format, values) {
  503. var result = format;
  504. for (var x = 0; x < values.length; x++) {
  505. result = result.replace(new RegExp('%' + (x + 1) + '\\$', 'g'), values[x]);
  506. }
  507. return result;
  508. }