Path: blob/trunk/third_party/closure/goog/editor/plugins/removeformatting.js
2868 views
// Copyright 2008 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.13// All Rights Reserved.1415/**16* @fileoverview Plugin to handle Remove Formatting.17*18*/1920goog.provide('goog.editor.plugins.RemoveFormatting');2122goog.require('goog.dom');23goog.require('goog.dom.NodeType');24goog.require('goog.dom.Range');25goog.require('goog.dom.TagName');26goog.require('goog.editor.BrowserFeature');27goog.require('goog.editor.Plugin');28goog.require('goog.editor.node');29goog.require('goog.editor.range');30goog.require('goog.string');31goog.require('goog.userAgent');32333435/**36* A plugin to handle removing formatting from selected text.37* @constructor38* @extends {goog.editor.Plugin}39* @final40*/41goog.editor.plugins.RemoveFormatting = function() {42goog.editor.Plugin.call(this);4344/**45* Optional function to perform remove formatting in place of the46* provided removeFormattingWorker_.47* @type {?function(string): string}48* @private49*/50this.optRemoveFormattingFunc_ = null;5152/**53* The key that this plugin triggers on when pressed with the platform54* modifier key. Can be set by calling {@link #setKeyboardShortcutKey}.55* @type {string}56* @private57*/58this.keyboardShortcutKey_ = ' ';59};60goog.inherits(goog.editor.plugins.RemoveFormatting, goog.editor.Plugin);616263/**64* The editor command this plugin in handling.65* @type {string}66*/67goog.editor.plugins.RemoveFormatting.REMOVE_FORMATTING_COMMAND =68'+removeFormat';697071/**72* Regular expression that matches a block tag name.73* @type {RegExp}74* @private75*/76goog.editor.plugins.RemoveFormatting.BLOCK_RE_ =77/^(DIV|TR|LI|BLOCKQUOTE|H\d|PRE|XMP)/;787980/**81* Appends a new line to a string buffer.82* @param {Array<string>} sb The string buffer to add to.83* @private84*/85goog.editor.plugins.RemoveFormatting.appendNewline_ = function(sb) {86sb.push('<br>');87};888990/**91* Create a new range delimited by the start point of the first range and92* the end point of the second range.93* @param {goog.dom.AbstractRange} startRange Use the start point of this94* range as the beginning of the new range.95* @param {goog.dom.AbstractRange} endRange Use the end point of this96* range as the end of the new range.97* @return {!goog.dom.AbstractRange} The new range.98* @private99*/100goog.editor.plugins.RemoveFormatting.createRangeDelimitedByRanges_ = function(101startRange, endRange) {102return goog.dom.Range.createFromNodes(103startRange.getStartNode(), startRange.getStartOffset(),104endRange.getEndNode(), endRange.getEndOffset());105};106107108/** @override */109goog.editor.plugins.RemoveFormatting.prototype.getTrogClassId = function() {110return 'RemoveFormatting';111};112113114/** @override */115goog.editor.plugins.RemoveFormatting.prototype.isSupportedCommand = function(116command) {117return command ==118goog.editor.plugins.RemoveFormatting.REMOVE_FORMATTING_COMMAND;119};120121122/** @override */123goog.editor.plugins.RemoveFormatting.prototype.execCommandInternal = function(124command, var_args) {125if (command ==126goog.editor.plugins.RemoveFormatting.REMOVE_FORMATTING_COMMAND) {127this.removeFormatting_();128}129};130131132/** @override */133goog.editor.plugins.RemoveFormatting.prototype.handleKeyboardShortcut =134function(e, key, isModifierPressed) {135if (!isModifierPressed) {136return false;137}138139// Disregard the shortcut if more than one modifier key is pressed140// because the user may have intended a different shortcut (for example OSX141// uses ctrlKey + metaKey + space to open the emoji picker).142if (e.metaKey && e.ctrlKey) {143return false;144}145146// Disregard the shortcut if the shift key is also pressed because the user147// may have intended a different shortcut (for example Chrome OS uses shiftKey148// + ctrlKey + space to toggle input languages.149if (e.shiftKey) {150return false;151}152153if (key == this.keyboardShortcutKey_) {154this.getFieldObject().execCommand(155goog.editor.plugins.RemoveFormatting.REMOVE_FORMATTING_COMMAND);156return true;157}158159return false;160};161162163/**164* @param {string} key165*/166goog.editor.plugins.RemoveFormatting.prototype.setKeyboardShortcutKey =167function(key) {168this.keyboardShortcutKey_ = key;169};170171172/**173* Removes formatting from the current selection. Removes basic formatting174* (B/I/U) using the browser's execCommand. Then extracts the html from the175* selection to convert, calls either a client's specified removeFormattingFunc176* callback or trogedit's general built-in removeFormattingWorker_,177* and then replaces the current selection with the converted text.178* @private179*/180goog.editor.plugins.RemoveFormatting.prototype.removeFormatting_ = function() {181var range = this.getFieldObject().getRange();182if (range.isCollapsed()) {183return;184}185186// Get the html to format and send it off for formatting. Built in187// removeFormat only strips some inline elements and some inline CSS styles188var convFunc = this.optRemoveFormattingFunc_ ||189goog.bind(this.removeFormattingWorker_, this);190this.convertSelectedHtmlText_(convFunc);191192// Do the execCommand last as it needs block elements removed to work193// properly on background/fontColor in FF. There are, unfortunately, still194// cases where background/fontColor are not removed here.195var doc = this.getFieldDomHelper().getDocument();196doc.execCommand('RemoveFormat', false, undefined);197198if (goog.editor.BrowserFeature.ADDS_NBSPS_IN_REMOVE_FORMAT) {199// WebKit converts spaces to non-breaking spaces when doing a RemoveFormat.200// See: https://bugs.webkit.org/show_bug.cgi?id=14062201this.convertSelectedHtmlText_(function(text) {202// This loses anything that might have legitimately been a non-breaking203// space, but that's better than the alternative of only having non-204// breaking spaces.205// Old versions of WebKit (Safari 3, Chrome 1) incorrectly match /u00A0206// and newer versions properly match .207var nbspRegExp =208goog.userAgent.isVersionOrHigher('528') ? / /g : /\u00A0/g;209return text.replace(nbspRegExp, ' ');210});211}212};213214215/**216* Finds the nearest ancestor of the node that is a table.217* @param {Node} nodeToCheck Node to search from.218* @return {Node} The table, or null if one was not found.219* @private220*/221goog.editor.plugins.RemoveFormatting.prototype.getTableAncestor_ = function(222nodeToCheck) {223var fieldElement = this.getFieldObject().getElement();224while (nodeToCheck && nodeToCheck != fieldElement) {225if (nodeToCheck.tagName == goog.dom.TagName.TABLE) {226return nodeToCheck;227}228nodeToCheck = nodeToCheck.parentNode;229}230return null;231};232233234/**235* Replaces the contents of the selection with html. Does its best to maintain236* the original selection. Also does its best to result in a valid DOM.237*238* TODO(user): See if there's any way to make this work on Ranges, and then239* move it into goog.editor.range. The Firefox implementation uses execCommand240* on the document, so must work on the actual selection.241*242* @param {string} html The html string to insert into the range.243* @private244*/245goog.editor.plugins.RemoveFormatting.prototype.pasteHtml_ = function(html) {246var range = this.getFieldObject().getRange();247248var dh = this.getFieldDomHelper();249// Use markers to set the extent of the selection so that we can reselect it250// afterwards. This works better than builtin range manipulation in FF and IE251// because their implementations are so self-inconsistent and buggy.252var startSpanId = goog.string.createUniqueString();253var endSpanId = goog.string.createUniqueString();254html = '<span id="' + startSpanId + '"></span>' + html + '<span id="' +255endSpanId + '"></span>';256var dummyNodeId = goog.string.createUniqueString();257var dummySpanText = '<span id="' + dummyNodeId + '"></span>';258259if (goog.editor.BrowserFeature.HAS_IE_RANGES) {260// IE's selection often doesn't include the outermost tags.261// We want to use pasteHTML to replace the range contents with the newly262// unformatted text, so we have to check to make sure we aren't just263// pasting into some stray tags. To do this, we first clear out the264// contents of the range and then delete all empty nodes parenting the now265// empty range. This way, the pasted contents are never re-embedded into266// formated nodes. Pasting purely empty html does not work, since IE moves267// the selection inside the next node, so we insert a dummy span.268var textRange = range.getTextRange(0).getBrowserRangeObject();269textRange.pasteHTML(dummySpanText);270var parent;271while ((parent = textRange.parentElement()) &&272goog.editor.node.isEmpty(parent) &&273!goog.editor.node.isEditableContainer(parent)) {274var tag = parent.nodeName;275// We can't remove these table tags as it will invalidate the table dom.276if (tag == goog.dom.TagName.TD || tag == goog.dom.TagName.TR ||277tag == goog.dom.TagName.TH) {278break;279}280281goog.dom.removeNode(parent);282}283textRange.pasteHTML(html);284var dummySpan = dh.getElement(dummyNodeId);285// If we entered the while loop above, the node has already been removed286// since it was a child of parent and parent was removed.287if (dummySpan) {288goog.dom.removeNode(dummySpan);289}290} else if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {291// insertHtml and range.insertNode don't merge blocks correctly.292// (e.g. if your selection spans two paragraphs)293dh.getDocument().execCommand('insertImage', false, dummyNodeId);294var dummyImageNodePattern = new RegExp('<[^<]*' + dummyNodeId + '[^>]*>');295var parent = this.getFieldObject().getRange().getContainerElement();296if (parent.nodeType == goog.dom.NodeType.TEXT) {297// Opera sometimes returns a text node here.298// TODO(user): perhaps we should modify getParentContainer?299parent = parent.parentNode;300}301302// We have to search up the DOM because in some cases, notably when303// selecting li's within a list, execCommand('insertImage') actually splits304// tags in such a way that parent that used to contain the selection does305// not contain inserted image.306while (!dummyImageNodePattern.test(parent.innerHTML)) {307parent = parent.parentNode;308}309310// Like the IE case above, sometimes the selection does not include the311// outermost tags. For Gecko, we have already expanded the range so that312// it does, so we can just replace the dummy image with the final html.313// For WebKit, we use the same approach as we do with IE - we314// inject a dummy span where we will eventually place the contents, and315// remove parentNodes of the span while they are empty.316317if (goog.userAgent.GECKO) {318// Escape dollars passed in second argument of String.proto.replace.319// And since we're using that to replace, we need to escape those as well,320// hence the 2*2 dollar signs.321goog.editor.node.replaceInnerHtml(322parent, parent.innerHTML.replace(323dummyImageNodePattern, html.replace(/\$/g, '$$$$')));324} else {325goog.editor.node.replaceInnerHtml(326parent,327parent.innerHTML.replace(dummyImageNodePattern, dummySpanText));328var dummySpan = dh.getElement(dummyNodeId);329parent = dummySpan;330while ((parent = dummySpan.parentNode) &&331goog.editor.node.isEmpty(parent) &&332!goog.editor.node.isEditableContainer(parent)) {333var tag = parent.nodeName;334// We can't remove these table tags as it will invalidate the table dom.335if (tag == goog.dom.TagName.TD || tag == goog.dom.TagName.TR ||336tag == goog.dom.TagName.TH) {337break;338}339340// We can't just remove parent since dummySpan is inside it, and we need341// to keep dummy span around for the replacement. So we move the342// dummySpan up as we go.343goog.dom.insertSiblingAfter(dummySpan, parent);344goog.dom.removeNode(parent);345}346goog.editor.node.replaceInnerHtml(347parent,348// Escape dollars passed in second argument of String.proto.replace349parent.innerHTML.replace(350new RegExp(dummySpanText, 'i'), html.replace(/\$/g, '$$$$')));351}352}353354var startSpan = dh.getElement(startSpanId);355var endSpan = dh.getElement(endSpanId);356goog.dom.Range357.createFromNodes(startSpan, 0, endSpan, endSpan.childNodes.length)358.select();359goog.dom.removeNode(startSpan);360goog.dom.removeNode(endSpan);361};362363364/**365* Gets the html inside the selection to send off for further processing.366*367* TODO(user): Make this general so that it can be moved into368* goog.editor.range. The main reason it can't be moved is because we need to369* get the range before we do the execCommand and continue to operate on that370* same range (reasons are documented above).371*372* @param {goog.dom.AbstractRange} range The selection.373* @return {string} The html string to format.374* @private375*/376goog.editor.plugins.RemoveFormatting.prototype.getHtmlText_ = function(range) {377var div = this.getFieldDomHelper().createDom(goog.dom.TagName.DIV);378var textRange = range.getBrowserRangeObject();379380if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {381// Get the text to convert.382div.appendChild(textRange.cloneContents());383} else if (goog.editor.BrowserFeature.HAS_IE_RANGES) {384// Trim the whitespace on the ends of the range, so that it the container385// will be the container of only the text content that we are changing.386// This gets around issues in IE where the spaces are included in the387// selection, but ignored sometimes by execCommand, and left orphaned.388var rngText = range.getText();389390// BRs get reported as \r\n, but only count as one character for moves.391// Adjust the string so our move counter is correct.392rngText = rngText.replace(/\r\n/g, '\r');393394var rngTextLength = rngText.length;395var left = rngTextLength - goog.string.trimLeft(rngText).length;396var right = rngTextLength - goog.string.trimRight(rngText).length;397398textRange.moveStart('character', left);399textRange.moveEnd('character', -right);400401var htmlText = textRange.htmlText;402// Check if in pretag and fix up formatting so that new lines are preserved.403if (textRange.queryCommandValue('formatBlock') == 'Formatted') {404htmlText = goog.string.newLineToBr(textRange.htmlText);405}406div.innerHTML = htmlText;407}408409// Get the innerHTML of the node instead of just returning the text above410// so that its properly html escaped.411return div.innerHTML;412};413414415/**416* Move the range so that it doesn't include any partially selected tables.417* @param {goog.dom.AbstractRange} range The range to adjust.418* @param {Node} startInTable Table node that the range starts in.419* @param {Node} endInTable Table node that the range ends in.420* @return {!goog.dom.SavedCaretRange} Range to use to restore the421* selection after we run our custom remove formatting.422* @private423*/424goog.editor.plugins.RemoveFormatting.prototype.adjustRangeForTables_ = function(425range, startInTable, endInTable) {426// Create placeholders for the current selection so we can restore it427// later.428var savedCaretRange = goog.editor.range.saveUsingNormalizedCarets(range);429430var startNode = range.getStartNode();431var startOffset = range.getStartOffset();432var endNode = range.getEndNode();433var endOffset = range.getEndOffset();434var dh = this.getFieldDomHelper();435436// Move start after the table.437if (startInTable) {438var textNode = dh.createTextNode('');439goog.dom.insertSiblingAfter(textNode, startInTable);440startNode = textNode;441startOffset = 0;442}443// Move end before the table.444if (endInTable) {445var textNode = dh.createTextNode('');446goog.dom.insertSiblingBefore(textNode, endInTable);447endNode = textNode;448endOffset = 0;449}450451goog.dom.Range.createFromNodes(startNode, startOffset, endNode, endOffset)452.select();453454return savedCaretRange;455};456457458/**459* Remove a caret from the dom and hide it in a safe place, so it can460* be restored later via restoreCaretsFromCave.461* @param {goog.dom.SavedCaretRange} caretRange The caret range to462* get the carets from.463* @param {boolean} isStart Whether this is the start or end caret.464* @private465*/466goog.editor.plugins.RemoveFormatting.prototype.putCaretInCave_ = function(467caretRange, isStart) {468var cavedCaret = goog.dom.removeNode(caretRange.getCaret(isStart));469if (isStart) {470this.startCaretInCave_ = cavedCaret;471} else {472this.endCaretInCave_ = cavedCaret;473}474};475476477/**478* Restore carets that were hidden away by adding them back into the dom.479* Note: this does not restore to the original dom location, as that480* will likely have been modified with remove formatting. The only481* guarantees here are that start will still be before end, and that482* they will be in the editable region. This should only be used when483* you don't actually intend to USE the caret again.484* @private485*/486goog.editor.plugins.RemoveFormatting.prototype.restoreCaretsFromCave_ =487function() {488// To keep start before end, we put the end caret at the bottom of the field489// and the start caret at the start of the field.490var field = this.getFieldObject().getElement();491if (this.startCaretInCave_) {492field.insertBefore(this.startCaretInCave_, field.firstChild);493this.startCaretInCave_ = null;494}495if (this.endCaretInCave_) {496field.appendChild(this.endCaretInCave_);497this.endCaretInCave_ = null;498}499};500501502/**503* Gets the html inside the current selection, passes it through the given504* conversion function, and puts it back into the selection.505*506* @param {function(string): string} convertFunc A conversion function that507* transforms an html string to new html string.508* @private509*/510goog.editor.plugins.RemoveFormatting.prototype.convertSelectedHtmlText_ =511function(convertFunc) {512var range = this.getFieldObject().getRange();513514// For multiple ranges, it is really hard to do our custom remove formatting515// without invalidating other ranges. So instead of always losing the516// content, this solution at least lets the browser do its own remove517// formatting which works correctly most of the time.518if (range.getTextRangeCount() > 1) {519return;520}521522if (goog.userAgent.GECKO || goog.userAgent.EDGE) {523// Determine if we need to handle tables, since they are special cases.524// If the selection is entirely within a table, there is no extra525// formatting removal we can do. If a table is fully selected, we will526// just blow it away. If a table is only partially selected, we can527// perform custom remove formatting only on the non table parts, since we528// we can't just remove the parts and paste back into it (eg. we can't529// inject html where a TR used to be).530// If the selection contains the table and more, this is automatically531// handled, but if just the table is selected, it can be tricky to figure532// this case out, because of the numerous ways selections can be formed -533// ex. if a table has a single tr with a single td with a single text node534// in it, and the selection is (textNode: 0), (textNode: nextNode.length)535// then the entire table is selected, even though the start and end aren't536// the table itself. We are truly inside a table if the expanded endpoints537// are still inside the table.538539// Expand the selection to include any outermost tags that weren't included540// in the selection, but have the same visible selection. Stop expanding541// if we reach the top level field.542var expandedRange =543goog.editor.range.expand(range, this.getFieldObject().getElement());544545var startInTable = this.getTableAncestor_(expandedRange.getStartNode());546var endInTable = this.getTableAncestor_(expandedRange.getEndNode());547548if (startInTable || endInTable) {549if (startInTable == endInTable) {550// We are fully contained in the same table, there is no extra551// remove formatting that we can do, just return and run browser552// formatting only.553return;554}555556// Adjust the range to not contain any partially selected tables, since557// we don't want to run our custom remove formatting on them.558var savedCaretRange =559this.adjustRangeForTables_(range, startInTable, endInTable);560561// Hack alert!!562// If start is not in a table, then the saved caret will get sent out563// for uber remove formatting, and it will get blown away. This is564// fine, except that we need to be able to re-create a range from the565// savedCaretRange later on. So, we just remove it from the dom, and566// put it back later so we can create a range later (not exactly in the567// same spot, but don't worry we don't actually try to use it later)568// and then it will be removed when we dispose the range.569if (!startInTable) {570this.putCaretInCave_(savedCaretRange, true);571}572if (!endInTable) {573this.putCaretInCave_(savedCaretRange, false);574}575576// Re-fetch the range, and re-expand it, since we just modified it.577range = this.getFieldObject().getRange();578expandedRange =579goog.editor.range.expand(range, this.getFieldObject().getElement());580}581582expandedRange.select();583range = expandedRange;584}585586// Convert the selected text to the format-less version, paste back into587// the selection.588var text = this.getHtmlText_(range);589this.pasteHtml_(convertFunc(text));590591if ((goog.userAgent.GECKO || goog.userAgent.EDGE) && savedCaretRange) {592// If we moved the selection, move it back so the user can't tell we did593// anything crazy and so the browser removeFormat that we call next594// will operate on the entire originally selected range.595range = this.getFieldObject().getRange();596this.restoreCaretsFromCave_();597var realSavedCaretRange = savedCaretRange.toAbstractRange();598var startRange = startInTable ? realSavedCaretRange : range;599var endRange = endInTable ? realSavedCaretRange : range;600var restoredRange =601goog.editor.plugins.RemoveFormatting.createRangeDelimitedByRanges_(602startRange, endRange);603restoredRange.select();604savedCaretRange.dispose();605}606};607608609/**610* Does a best-effort attempt at clobbering all formatting that the611* browser's execCommand couldn't clobber without being totally inefficient.612* Attempts to convert visual line breaks to BRs. Leaves anchors that contain an613* href and images.614* Adapted from Gmail's MessageUtil's htmlToPlainText. http://go/messageutil.js615* @param {string} html The original html of the message.616* @return {string} The unformatted html, which is just text, br's, anchors and617* images.618* @private619*/620goog.editor.plugins.RemoveFormatting.prototype.removeFormattingWorker_ =621function(html) {622var el = goog.dom.createElement(goog.dom.TagName.DIV);623el.innerHTML = html;624625// Put everything into a string buffer to avoid lots of expensive string626// concatenation along the way.627var sb = [];628var stack = [el.childNodes, 0];629630// Keep separate stacks for places where we need to keep track of631// how deeply embedded we are. These are analogous to the general stack.632var preTagStack = [];633var preTagLevel = 0; // Length of the prestack.634var tableStack = [];635var tableLevel = 0;636637// sp = stack pointer, pointing to the stack array.638// decrement by 2 since the stack alternates node lists and639// processed node counts640for (var sp = 0; sp >= 0; sp -= 2) {641// Check if we should pop the table level.642var changedLevel = false;643while (tableLevel > 0 && sp <= tableStack[tableLevel - 1]) {644tableLevel--;645changedLevel = true;646}647if (changedLevel) {648goog.editor.plugins.RemoveFormatting.appendNewline_(sb);649}650651652// Check if we should pop the <pre>/<xmp> level.653changedLevel = false;654while (preTagLevel > 0 && sp <= preTagStack[preTagLevel - 1]) {655preTagLevel--;656changedLevel = true;657}658if (changedLevel) {659goog.editor.plugins.RemoveFormatting.appendNewline_(sb);660}661662// The list of of nodes to process at the current stack level.663var nodeList = stack[sp];664// The number of nodes processed so far, stored in the stack immediately665// following the node list for that stack level.666var numNodesProcessed = stack[sp + 1];667668while (numNodesProcessed < nodeList.length) {669var node = nodeList[numNodesProcessed++];670var nodeName = node.nodeName;671672var formatted = this.getValueForNode(node);673if (goog.isDefAndNotNull(formatted)) {674sb.push(formatted);675continue;676}677678// TODO(user): Handle case 'EMBED' and case 'OBJECT'.679switch (nodeName) {680case '#text':681// Note that IE does not preserve whitespace in the dom682// values, even in a pre tag, so this is useless for IE.683var nodeValue = preTagLevel > 0 ?684node.nodeValue :685goog.string.stripNewlines(node.nodeValue);686nodeValue = goog.string.htmlEscape(nodeValue);687sb.push(nodeValue);688continue;689690case String(goog.dom.TagName.P):691goog.editor.plugins.RemoveFormatting.appendNewline_(sb);692goog.editor.plugins.RemoveFormatting.appendNewline_(sb);693break; // break (not continue) so that child nodes are processed.694695case String(goog.dom.TagName.BR):696goog.editor.plugins.RemoveFormatting.appendNewline_(sb);697continue;698699case String(goog.dom.TagName.TABLE):700goog.editor.plugins.RemoveFormatting.appendNewline_(sb);701tableStack[tableLevel++] = sp;702break;703704case String(goog.dom.TagName.PRE):705case 'XMP':706// This doesn't fully handle xmp, since707// it doesn't actually ignore tags within the xmp tag.708preTagStack[preTagLevel++] = sp;709break;710711case String(goog.dom.TagName.STYLE):712case String(goog.dom.TagName.SCRIPT):713case String(goog.dom.TagName.SELECT):714continue;715716case String(goog.dom.TagName.A):717if (node.href && node.href != '') {718sb.push("<a href='");719sb.push(node.href);720sb.push("'>");721sb.push(this.removeFormattingWorker_(node.innerHTML));722sb.push('</a>');723continue; // Children taken care of.724} else {725break; // Take care of the children.726}727728case String(goog.dom.TagName.IMG):729sb.push("<img src='");730sb.push(node.src);731sb.push("'");732// border=0 is a common way to not show a blue border around an image733// that is wrapped by a link. If we remove that, the blue border will734// show up, which to the user looks like adding format, not removing.735if (node.border == '0') {736sb.push(" border='0'");737}738sb.push('>');739continue;740741case String(goog.dom.TagName.TD):742// Don't add a space for the first TD, we only want spaces to743// separate td's.744if (node.previousSibling) {745sb.push(' ');746}747break;748749case String(goog.dom.TagName.TR):750// Don't add a newline for the first TR.751if (node.previousSibling) {752goog.editor.plugins.RemoveFormatting.appendNewline_(sb);753}754break;755756case String(goog.dom.TagName.DIV):757var parent = node.parentNode;758if (parent.firstChild == node &&759goog.editor.plugins.RemoveFormatting.BLOCK_RE_.test(760parent.tagName)) {761// If a DIV is the first child of another element that itself is a762// block element, the DIV does not add a new line.763break;764}765// Otherwise, the DIV does add a new line. Fall through.766767default:768if (goog.editor.plugins.RemoveFormatting.BLOCK_RE_.test(nodeName)) {769goog.editor.plugins.RemoveFormatting.appendNewline_(sb);770}771}772773// Recurse down the node.774var children = node.childNodes;775if (children.length > 0) {776// Push the current state on the stack.777stack[sp++] = nodeList;778stack[sp++] = numNodesProcessed;779780// Iterate through the children nodes.781nodeList = children;782numNodesProcessed = 0;783}784}785}786787// Replace with white space.788return goog.string.normalizeSpaces(sb.join(''));789};790791792/**793* Handle per node special processing if necessary. If this function returns794* null then standard cleanup is applied. Otherwise this node and all children795* are assumed to be cleaned.796* NOTE(user): If an alternate RemoveFormatting processor is provided797* (setRemoveFormattingFunc()), this will no longer work.798* @param {Element} node The node to clean.799* @return {?string} The HTML strig representation of the cleaned data.800*/801goog.editor.plugins.RemoveFormatting.prototype.getValueForNode = function(802node) {803return null;804};805806807/**808* Sets a function to be used for remove formatting.809* @param {function(string): string} removeFormattingFunc - A function that810* takes a string of html and returns a string of html that does any other811* formatting changes desired. Use this only if trogedit's behavior doesn't812* meet your needs.813*/814goog.editor.plugins.RemoveFormatting.prototype.setRemoveFormattingFunc =815function(removeFormattingFunc) {816this.optRemoveFormattingFunc_ = removeFormattingFunc;817};818819820