| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640 |
- /*
- * 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<str.length; i++) {
- re += '\\' + str.charAt(i);
- }
- return '(?:(?:[\xa0' + re + '])|(?:\\-\\-))+';
- };
- AtDCore.prototype.processXML = function(responseXML) {
- /* types of errors to ignore */
- var types = {};
- this.map(this.ignore_types, function(type) {
- types[type] = 1;
- });
- /* save suggestions in the editor object */
- this.suggestions = [];
- /* process through the errors */
- var errors = responseXML.getElementsByTagName('error');
- /* words to mark */
- var grammarErrors = [];
- var spellingErrors = [];
- var enrichment = [];
- for (var i = 0; i < errors.length; i++) {
- if (errors[i].getElementsByTagName('string').item(0).firstChild !== null) {
- var errorString = errors[i].getElementsByTagName('string').item(0).firstChild.data;
- var errorType = errors[i].getElementsByTagName('type').item(0).firstChild.data;
- var errorDescription = errors[i].getElementsByTagName('description').item(0).firstChild.data;
- var errorContext;
- if (errors[i].getElementsByTagName('precontext').item(0).firstChild !== null) {
- errorContext = errors[i].getElementsByTagName('precontext').item(0).firstChild.data;
- } else {
- errorContext = '';
- }
- /* create a hashtable with information about the error in the editor object, we will use this later
- to populate a popup menu with information and suggestions about the error */
- if (this.ignore_strings[errorString] === undefined) {
- var suggestion = {};
- suggestion['description'] = errorDescription;
- suggestion['suggestions'] = [];
- /* used to find suggestions when a highlighted error is clicked on */
- suggestion['matcher'] = new RegExp('^' + errorString.replace(/\s+/, this._getSeparators()) + '$');
- suggestion['context'] = errorContext;
- suggestion['string'] = errorString;
- suggestion['type'] = errorType;
- this.suggestions.push(suggestion);
- if (errors[i].getElementsByTagName('suggestions').item(0) !== null) {
- var suggestions = errors[i].getElementsByTagName('suggestions').item(0).getElementsByTagName('option');
- for (var j = 0; j < suggestions.length; j++) {
- suggestion['suggestions'].push(suggestions[j].firstChild.data);
- }
- }
- /* setup the more info url */
- if (errors[i].getElementsByTagName('url').item(0) !== null) {
- var errorUrl = errors[i].getElementsByTagName('url').item(0).firstChild.data;
- suggestion['moreinfo'] = errorUrl + '&theme=tinymce';
- }
- if (types[errorDescription] === undefined) {
- if (errorType === 'suggestion') {
- enrichment.push({ word: errorString, pre: errorContext });
- }
- if (errorType === 'grammar') {
- grammarErrors.push({ word: errorString, pre: errorContext });
- }
- }
- if (errorType === 'spelling' || errorDescription === 'Homophone') {
- spellingErrors.push({ word: errorString, pre: errorContext });
- }
- if (errorDescription === 'Cliches') {
- suggestion['description'] = 'Clichés'; /* done here for backwards compatability with current user settings */
- }
- if (errorDescription === 'Spelling') {
- suggestion['description'] = this.getLang('menu_title_spelling', 'Spelling');
- }
- if (errorDescription === 'Repeated Word') {
- suggestion['description'] = this.getLang('menu_title_repeated_word', 'Repeated Word');
- }
- if (errorDescription === 'Did you mean...') {
- suggestion['description'] = this.getLang('menu_title_confused_word', 'Did you mean...');
- }
- } // end if ignore[errorString] == undefined
- } // end if
- } // end for loop
- var errorStruct;
- var ecount = spellingErrors.length + grammarErrors.length + enrichment.length;
- if (ecount > 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 = '<span class="mceItemHidden"' + bogus + '> </span>',
- 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, '<span class="'+error.type+'" pre="'+previous+'"' + bogus + '>$&</span>' ]);
- 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( '<span />' );
- 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;
- }
|