/* * atd.core.js - A building block to create a front-end for AtD * Author : Raphael Mudge, Automattic * License : LGPL * Project : http://www.afterthedeadline.com/developers.slp * Contact : raffi@automattic.com */ /* jshint sub: true, devel: true, onevar: false, smarttabs: true, loopfunc: true */ /* exported EXPORTED_SYMBOLS, atd_sprintf */ /* EXPORTED_SYMBOLS is set so this file can be a JavaScript Module */ var EXPORTED_SYMBOLS = ['AtDCore']; function AtDCore() { /* these are the categories of errors AtD should ignore */ this.ignore_types = ['Bias Language', 'Cliches', 'Complex Expression', 'Diacritical Marks', 'Double Negatives', 'Hidden Verbs', 'Jargon Language', 'Passive voice', 'Phrases to Avoid', 'Redundant Expression']; /* these are the phrases AtD should ignore */ this.ignore_strings = {}; /* Localized strings */ // Back-compat, not used this.i18n = {}; } /* * Internationalization Functions */ AtDCore.prototype.getLang = function( key, defaultk ) { return ( window.AtD_l10n_r0ar && window.AtD_l10n_r0ar[key] ) || defaultk; }; AtDCore.prototype.addI18n = function( obj ) { // Back-compat window.AtD_l10n_r0ar = obj; }; /* * Setters */ AtDCore.prototype.setIgnoreStrings = function(string) { var parent = this; this.map(string.split(/,\s*/g), function(string) { parent.ignore_strings[string] = 1; }); }; AtDCore.prototype.showTypes = function(string) { var show_types = string.split(/,\s*/g); var types = {}; /* set some default types that we want to make optional */ /* grammar checker options */ types['Double Negatives'] = 1; types['Hidden Verbs'] = 1; types['Passive voice'] = 1; types['Bias Language'] = 1; /* style checker options */ types['Cliches'] = 1; types['Complex Expression'] = 1; types['Diacritical Marks'] = 1; types['Jargon Language'] = 1; types['Phrases to Avoid'] = 1; types['Redundant Expression'] = 1; var ignore_types = []; this.map(show_types, function(string) { types[string] = undefined; }); this.map(this.ignore_types, function(string) { if (types[string] !== undefined) { ignore_types.push(string); } }); this.ignore_types = ignore_types; }; /* * Error Parsing Code */ AtDCore.prototype.makeError = function(error_s, tokens, type, seps/*, pre*/) { var struct = {}; struct.type = type; struct.string = error_s; struct.tokens = tokens; if (new RegExp('\\b' + error_s + '\\b').test(error_s)) { struct.regexp = new RegExp('(?!'+error_s+'<)\\b' + error_s.replace(/\s+/g, seps) + '\\b'); } else if (new RegExp(error_s + '\\b').test(error_s)) { struct.regexp = new RegExp('(?!'+error_s+'<)' + error_s.replace(/\s+/g, seps) + '\\b'); } else if (new RegExp('\\b' + error_s).test(error_s)) { struct.regexp = new RegExp('(?!'+error_s+'<)\\b' + error_s.replace(/\s+/g, seps)); } else { struct.regexp = new RegExp('(?!'+error_s+'<)' + error_s.replace(/\s+/g, seps)); } struct.used = false; /* flag whether we've used this rule or not */ return struct; }; AtDCore.prototype.addToErrorStructure = function(errors, list, type, seps) { var parent = this; this.map(list, function(error) { var tokens = error['word'].split(/\s+/); var pre = error['pre']; var first = tokens[0]; if (errors['__' + first] === undefined) { errors['__' + first] = {}; errors['__' + first].pretoks = {}; errors['__' + first].defaults = []; } if (pre === '') { errors['__' + first].defaults.push(parent.makeError(error['word'], tokens, type, seps, pre)); } else { if (errors['__' + first].pretoks['__' + pre] === undefined) { errors['__' + first].pretoks['__' + pre] = []; } errors['__' + first].pretoks['__' + pre].push(parent.makeError(error['word'], tokens, type, seps, pre)); } }); }; AtDCore.prototype.buildErrorStructure = function(spellingList, enrichmentList, grammarList) { var seps = this._getSeparators(); var errors = {}; this.addToErrorStructure(errors, spellingList, 'hiddenSpellError', seps); this.addToErrorStructure(errors, grammarList, 'hiddenGrammarError', seps); this.addToErrorStructure(errors, enrichmentList, 'hiddenSuggestion', seps); return errors; }; AtDCore.prototype._getSeparators = function() { var re = '', i; var str = '"s!#$%&()*+,./:;<=>?@[\\]^_{|}'; // Build word separator regexp for (i=0; i 0) { errorStruct = this.buildErrorStructure(spellingErrors, enrichment, grammarErrors); } else { errorStruct = undefined; } /* save some state in this object, for retrieving suggestions later */ return { errors: errorStruct, count: ecount, suggestions: this.suggestions }; }; AtDCore.prototype.findSuggestion = function(element) { var text = element.innerHTML; var context = ( this.getAttrib(element, 'pre') + '' ).replace(/[\\,!\\?\\."\s]/g, ''); if (this.getAttrib(element, 'pre') === undefined) { alert(element.innerHTML); } var errorDescription; var len = this.suggestions.length; for (var i = 0; i < len; i++) { if ((context === '' || context === this.suggestions[i]['context']) && this.suggestions[i]['matcher'].test(text)) { errorDescription = this.suggestions[i]; break; } } return errorDescription; }; /* * TokenIterator class */ function TokenIterator(tokens) { this.tokens = tokens; this.index = 0; this.count = 0; this.last = 0; } TokenIterator.prototype.next = function() { var current = this.tokens[this.index]; this.count = this.last; this.last += current.length + 1; this.index++; /* strip single quotes from token, AtD does this when presenting errors */ if (current !== '') { if (current[0] === '\'') { current = current.substring(1, current.length); } if (current[current.length - 1] === '\'') { current = current.substring(0, current.length - 1); } } return current; }; TokenIterator.prototype.hasNext = function() { return this.index < this.tokens.length; }; TokenIterator.prototype.hasNextN = function(n) { return (this.index + n) < this.tokens.length; }; TokenIterator.prototype.skip = function(m, n) { this.index += m; this.last += n; if (this.index < this.tokens.length) { this.count = this.last - this.tokens[this.index].length; } }; TokenIterator.prototype.getCount = function() { return this.count; }; TokenIterator.prototype.peek = function(n) { var peepers = []; var end = this.index + n; for (var x = this.index; x < end; x++) { peepers.push(this.tokens[x]); } return peepers; }; /* * code to manage highlighting of errors */ AtDCore.prototype.markMyWords = function(container_nodes, errors) { var seps = new RegExp(this._getSeparators()), nl = [], ecount = 0, /* track number of highlighted errors */ parent = this, bogus = this._isTinyMCE ? ' data-mce-bogus="1"' : '', emptySpan = ' ', textOnlyMode; /** * Split a text node into an ordered list of siblings: * - text node to the left of the match * - the element replacing the match * - text node to the right of the match * * We have to leave the text to the left and right of the match alone * in order to prevent XSS * * @return array */ function splitTextNode( textnode, regexp, replacement ) { var text = textnode.nodeValue, index = text.search( regexp ), match = text.match( regexp ), captured = [], cursor; if ( index < 0 || ! match.length ) { return [ textnode ]; } if ( index > 0 ) { // capture left text node captured.push( document.createTextNode( text.substr( 0, index ) ) ); } // capture the replacement of the matched string captured.push( parent.create( match[0].replace( regexp, replacement ) ) ); cursor = index + match[0].length; if ( cursor < text.length ) { // capture right text node captured.push( document.createTextNode( text.substr( cursor ) ) ); } return captured; } function _isInPre( node ) { if ( node ) { while ( node.parentNode ) { if ( node.nodeName === 'PRE' ) { return true; } node = node.parentNode; } } return false; } /* Collect all text nodes */ /* Our goal--ignore nodes that are already wrapped */ this._walk( container_nodes, function( n ) { if ( n.nodeType === 3 && ! parent.isMarkedNode( n ) && ! _isInPre( n ) ) { nl.push( n ); } }); /* walk through the relevant nodes */ var iterator; this.map( nl, function( n ) { var v; if ( n.nodeType === 3 ) { v = n.nodeValue; /* we don't want to mangle the HTML so use the actual encoded string */ var tokens = n.nodeValue.split( seps ); /* split on the unencoded string so we get access to quotes as " */ var previous = ''; var doReplaces = []; iterator = new TokenIterator(tokens); while ( iterator.hasNext() ) { var token = iterator.next(); var current = errors['__' + token]; var defaults; if ( current !== undefined && current.pretoks !== undefined ) { defaults = current.defaults; current = current.pretoks['__' + previous]; var done = false; var prev, curr; prev = v.substr(0, iterator.getCount()); curr = v.substr(prev.length, v.length); var checkErrors = function( error ) { if ( error !== undefined && ! error.used && foundStrings[ '__' + error.string ] === undefined && error.regexp.test( curr ) ) { foundStrings[ '__' + error.string ] = 1; doReplaces.push([ error.regexp, '$&' ]); error.used = true; done = true; } }; // jshint ignore:line var foundStrings = {}; if (current !== undefined) { previous = previous + ' '; parent.map(current, checkErrors); } if (!done) { previous = ''; parent.map(defaults, checkErrors); } } previous = token; } // end while /* do the actual replacements on this span */ if ( doReplaces.length > 0 ) { var newNode = n; for ( var x = 0; x < doReplaces.length; x++ ) { var regexp = doReplaces[x][0], result = doReplaces[x][1]; /* it's assumed that this function is only being called on text nodes (nodeType == 3), the iterating is necessary because eventually the whole thing gets wrapped in an mceItemHidden span and from there it's necessary to handle each node individually. */ var bringTheHurt = function( node ) { var span, splitNodes; if ( node.nodeType === 3 ) { ecount++; /* sometimes IE likes to ignore the space between two spans, solution is to insert a placeholder span with a non-breaking space. The markup removal code substitutes this span for a space later */ if ( parent.isIE() && node.nodeValue.length > 0 && node.nodeValue.substr(0, 1) === ' ' ) { return parent.create( emptySpan + node.nodeValue.substr( 1, node.nodeValue.length - 1 ).replace( regexp, result ), false ); } else { if ( textOnlyMode ) { return parent.create( node.nodeValue.replace( regexp, result ), false ); } span = parent.create( '' ); if ( typeof textOnlyMode === 'undefined' ) { // cache this to avoid adding / removing nodes unnecessarily textOnlyMode = typeof span.appendChild !== 'function'; if ( textOnlyMode ) { parent.remove( span ); return parent.create( node.nodeValue.replace( regexp, result ), false ); } } // "Visual" mode splitNodes = splitTextNode( node, regexp, result ); for ( var i = 0; i < splitNodes.length; i++ ) { span.appendChild( splitNodes[i] ); } node = span; return node; } } else { var contents = parent.contents(node); for ( var y = 0; y < contents.length; y++ ) { if ( contents[y].nodeType === 3 && regexp.test( contents[y].nodeValue ) ) { var nnode; if ( parent.isIE() && contents[y].nodeValue.length > 0 && contents[y].nodeValue.substr(0, 1) === ' ') { nnode = parent.create( emptySpan + contents[y].nodeValue.substr( 1, contents[y].nodeValue.length - 1 ).replace( regexp, result ), true ); } else { nnode = parent.create( contents[y].nodeValue.replace( regexp, result ), true ); } parent.replaceWith( contents[y], nnode ); parent.removeParent( nnode ); ecount++; return node; /* we did a replacement so we can call it quits, errors only get used once */ } } return node; } }; // jshint ignore:line newNode = bringTheHurt(newNode); } parent.replaceWith(n, newNode); } } }); return ecount; }; AtDCore.prototype._walk = function(elements, f) { var i; for (i = 0; i < elements.length; i++) { f.call(f, elements[i]); this._walk(this.contents(elements[i]), f); } }; AtDCore.prototype.removeWords = function(node, w) { var count = 0; var parent = this; this.map(this.findSpans(node).reverse(), function(n) { if (n && (parent.isMarkedNode(n) || parent.hasClass(n, 'mceItemHidden') || parent.isEmptySpan(n)) ) { if (n.innerHTML === ' ') { var nnode = document.createTextNode(' '); /* hax0r */ parent.replaceWith(n, nnode); } else if (!w || n.innerHTML === w) { parent.removeParent(n); count++; } } }); return count; }; AtDCore.prototype.isEmptySpan = function(node) { return (this.getAttrib(node, 'class') === '' && this.getAttrib(node, 'style') === '' && this.getAttrib(node, 'id') === '' && !this.hasClass(node, 'Apple-style-span') && this.getAttrib(node, 'mce_name') === ''); }; AtDCore.prototype.isMarkedNode = function(node) { return (this.hasClass(node, 'hiddenGrammarError') || this.hasClass(node, 'hiddenSpellError') || this.hasClass(node, 'hiddenSuggestion')); }; /* * Context Menu Helpers */ AtDCore.prototype.applySuggestion = function(element, suggestion) { if (suggestion === '(omit)') { this.remove(element); } else { var node = this.create(suggestion); this.replaceWith(element, node); this.removeParent(node); } }; /* * Check for an error */ AtDCore.prototype.hasErrorMessage = function(xmlr) { return (xmlr !== undefined && xmlr.getElementsByTagName('message').item(0) !== null); }; AtDCore.prototype.getErrorMessage = function(xmlr) { return xmlr.getElementsByTagName('message').item(0); }; /* this should always be an error, alas... not practical */ AtDCore.prototype.isIE = function() { return navigator.appName === 'Microsoft Internet Explorer'; }; // TODO: this doesn't seem used anywhere in AtD, moved here from install_atd_l10n.js for eventual back-compat /* a quick poor man's sprintf */ function atd_sprintf(format, values) { var result = format; for (var x = 0; x < values.length; x++) { result = result.replace(new RegExp('%' + (x + 1) + '\\$', 'g'), values[x]); } return result; }