Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/third_party/closure/goog/editor/plugins/removeformatting.js
2868 views
1
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
// http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS-IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
// All Rights Reserved.
15
16
/**
17
* @fileoverview Plugin to handle Remove Formatting.
18
*
19
*/
20
21
goog.provide('goog.editor.plugins.RemoveFormatting');
22
23
goog.require('goog.dom');
24
goog.require('goog.dom.NodeType');
25
goog.require('goog.dom.Range');
26
goog.require('goog.dom.TagName');
27
goog.require('goog.editor.BrowserFeature');
28
goog.require('goog.editor.Plugin');
29
goog.require('goog.editor.node');
30
goog.require('goog.editor.range');
31
goog.require('goog.string');
32
goog.require('goog.userAgent');
33
34
35
36
/**
37
* A plugin to handle removing formatting from selected text.
38
* @constructor
39
* @extends {goog.editor.Plugin}
40
* @final
41
*/
42
goog.editor.plugins.RemoveFormatting = function() {
43
goog.editor.Plugin.call(this);
44
45
/**
46
* Optional function to perform remove formatting in place of the
47
* provided removeFormattingWorker_.
48
* @type {?function(string): string}
49
* @private
50
*/
51
this.optRemoveFormattingFunc_ = null;
52
53
/**
54
* The key that this plugin triggers on when pressed with the platform
55
* modifier key. Can be set by calling {@link #setKeyboardShortcutKey}.
56
* @type {string}
57
* @private
58
*/
59
this.keyboardShortcutKey_ = ' ';
60
};
61
goog.inherits(goog.editor.plugins.RemoveFormatting, goog.editor.Plugin);
62
63
64
/**
65
* The editor command this plugin in handling.
66
* @type {string}
67
*/
68
goog.editor.plugins.RemoveFormatting.REMOVE_FORMATTING_COMMAND =
69
'+removeFormat';
70
71
72
/**
73
* Regular expression that matches a block tag name.
74
* @type {RegExp}
75
* @private
76
*/
77
goog.editor.plugins.RemoveFormatting.BLOCK_RE_ =
78
/^(DIV|TR|LI|BLOCKQUOTE|H\d|PRE|XMP)/;
79
80
81
/**
82
* Appends a new line to a string buffer.
83
* @param {Array<string>} sb The string buffer to add to.
84
* @private
85
*/
86
goog.editor.plugins.RemoveFormatting.appendNewline_ = function(sb) {
87
sb.push('<br>');
88
};
89
90
91
/**
92
* Create a new range delimited by the start point of the first range and
93
* the end point of the second range.
94
* @param {goog.dom.AbstractRange} startRange Use the start point of this
95
* range as the beginning of the new range.
96
* @param {goog.dom.AbstractRange} endRange Use the end point of this
97
* range as the end of the new range.
98
* @return {!goog.dom.AbstractRange} The new range.
99
* @private
100
*/
101
goog.editor.plugins.RemoveFormatting.createRangeDelimitedByRanges_ = function(
102
startRange, endRange) {
103
return goog.dom.Range.createFromNodes(
104
startRange.getStartNode(), startRange.getStartOffset(),
105
endRange.getEndNode(), endRange.getEndOffset());
106
};
107
108
109
/** @override */
110
goog.editor.plugins.RemoveFormatting.prototype.getTrogClassId = function() {
111
return 'RemoveFormatting';
112
};
113
114
115
/** @override */
116
goog.editor.plugins.RemoveFormatting.prototype.isSupportedCommand = function(
117
command) {
118
return command ==
119
goog.editor.plugins.RemoveFormatting.REMOVE_FORMATTING_COMMAND;
120
};
121
122
123
/** @override */
124
goog.editor.plugins.RemoveFormatting.prototype.execCommandInternal = function(
125
command, var_args) {
126
if (command ==
127
goog.editor.plugins.RemoveFormatting.REMOVE_FORMATTING_COMMAND) {
128
this.removeFormatting_();
129
}
130
};
131
132
133
/** @override */
134
goog.editor.plugins.RemoveFormatting.prototype.handleKeyboardShortcut =
135
function(e, key, isModifierPressed) {
136
if (!isModifierPressed) {
137
return false;
138
}
139
140
// Disregard the shortcut if more than one modifier key is pressed
141
// because the user may have intended a different shortcut (for example OSX
142
// uses ctrlKey + metaKey + space to open the emoji picker).
143
if (e.metaKey && e.ctrlKey) {
144
return false;
145
}
146
147
// Disregard the shortcut if the shift key is also pressed because the user
148
// may have intended a different shortcut (for example Chrome OS uses shiftKey
149
// + ctrlKey + space to toggle input languages.
150
if (e.shiftKey) {
151
return false;
152
}
153
154
if (key == this.keyboardShortcutKey_) {
155
this.getFieldObject().execCommand(
156
goog.editor.plugins.RemoveFormatting.REMOVE_FORMATTING_COMMAND);
157
return true;
158
}
159
160
return false;
161
};
162
163
164
/**
165
* @param {string} key
166
*/
167
goog.editor.plugins.RemoveFormatting.prototype.setKeyboardShortcutKey =
168
function(key) {
169
this.keyboardShortcutKey_ = key;
170
};
171
172
173
/**
174
* Removes formatting from the current selection. Removes basic formatting
175
* (B/I/U) using the browser's execCommand. Then extracts the html from the
176
* selection to convert, calls either a client's specified removeFormattingFunc
177
* callback or trogedit's general built-in removeFormattingWorker_,
178
* and then replaces the current selection with the converted text.
179
* @private
180
*/
181
goog.editor.plugins.RemoveFormatting.prototype.removeFormatting_ = function() {
182
var range = this.getFieldObject().getRange();
183
if (range.isCollapsed()) {
184
return;
185
}
186
187
// Get the html to format and send it off for formatting. Built in
188
// removeFormat only strips some inline elements and some inline CSS styles
189
var convFunc = this.optRemoveFormattingFunc_ ||
190
goog.bind(this.removeFormattingWorker_, this);
191
this.convertSelectedHtmlText_(convFunc);
192
193
// Do the execCommand last as it needs block elements removed to work
194
// properly on background/fontColor in FF. There are, unfortunately, still
195
// cases where background/fontColor are not removed here.
196
var doc = this.getFieldDomHelper().getDocument();
197
doc.execCommand('RemoveFormat', false, undefined);
198
199
if (goog.editor.BrowserFeature.ADDS_NBSPS_IN_REMOVE_FORMAT) {
200
// WebKit converts spaces to non-breaking spaces when doing a RemoveFormat.
201
// See: https://bugs.webkit.org/show_bug.cgi?id=14062
202
this.convertSelectedHtmlText_(function(text) {
203
// This loses anything that might have legitimately been a non-breaking
204
// space, but that's better than the alternative of only having non-
205
// breaking spaces.
206
// Old versions of WebKit (Safari 3, Chrome 1) incorrectly match /u00A0
207
// and newer versions properly match &nbsp;.
208
var nbspRegExp =
209
goog.userAgent.isVersionOrHigher('528') ? /&nbsp;/g : /\u00A0/g;
210
return text.replace(nbspRegExp, ' ');
211
});
212
}
213
};
214
215
216
/**
217
* Finds the nearest ancestor of the node that is a table.
218
* @param {Node} nodeToCheck Node to search from.
219
* @return {Node} The table, or null if one was not found.
220
* @private
221
*/
222
goog.editor.plugins.RemoveFormatting.prototype.getTableAncestor_ = function(
223
nodeToCheck) {
224
var fieldElement = this.getFieldObject().getElement();
225
while (nodeToCheck && nodeToCheck != fieldElement) {
226
if (nodeToCheck.tagName == goog.dom.TagName.TABLE) {
227
return nodeToCheck;
228
}
229
nodeToCheck = nodeToCheck.parentNode;
230
}
231
return null;
232
};
233
234
235
/**
236
* Replaces the contents of the selection with html. Does its best to maintain
237
* the original selection. Also does its best to result in a valid DOM.
238
*
239
* TODO(user): See if there's any way to make this work on Ranges, and then
240
* move it into goog.editor.range. The Firefox implementation uses execCommand
241
* on the document, so must work on the actual selection.
242
*
243
* @param {string} html The html string to insert into the range.
244
* @private
245
*/
246
goog.editor.plugins.RemoveFormatting.prototype.pasteHtml_ = function(html) {
247
var range = this.getFieldObject().getRange();
248
249
var dh = this.getFieldDomHelper();
250
// Use markers to set the extent of the selection so that we can reselect it
251
// afterwards. This works better than builtin range manipulation in FF and IE
252
// because their implementations are so self-inconsistent and buggy.
253
var startSpanId = goog.string.createUniqueString();
254
var endSpanId = goog.string.createUniqueString();
255
html = '<span id="' + startSpanId + '"></span>' + html + '<span id="' +
256
endSpanId + '"></span>';
257
var dummyNodeId = goog.string.createUniqueString();
258
var dummySpanText = '<span id="' + dummyNodeId + '"></span>';
259
260
if (goog.editor.BrowserFeature.HAS_IE_RANGES) {
261
// IE's selection often doesn't include the outermost tags.
262
// We want to use pasteHTML to replace the range contents with the newly
263
// unformatted text, so we have to check to make sure we aren't just
264
// pasting into some stray tags. To do this, we first clear out the
265
// contents of the range and then delete all empty nodes parenting the now
266
// empty range. This way, the pasted contents are never re-embedded into
267
// formated nodes. Pasting purely empty html does not work, since IE moves
268
// the selection inside the next node, so we insert a dummy span.
269
var textRange = range.getTextRange(0).getBrowserRangeObject();
270
textRange.pasteHTML(dummySpanText);
271
var parent;
272
while ((parent = textRange.parentElement()) &&
273
goog.editor.node.isEmpty(parent) &&
274
!goog.editor.node.isEditableContainer(parent)) {
275
var tag = parent.nodeName;
276
// We can't remove these table tags as it will invalidate the table dom.
277
if (tag == goog.dom.TagName.TD || tag == goog.dom.TagName.TR ||
278
tag == goog.dom.TagName.TH) {
279
break;
280
}
281
282
goog.dom.removeNode(parent);
283
}
284
textRange.pasteHTML(html);
285
var dummySpan = dh.getElement(dummyNodeId);
286
// If we entered the while loop above, the node has already been removed
287
// since it was a child of parent and parent was removed.
288
if (dummySpan) {
289
goog.dom.removeNode(dummySpan);
290
}
291
} else if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
292
// insertHtml and range.insertNode don't merge blocks correctly.
293
// (e.g. if your selection spans two paragraphs)
294
dh.getDocument().execCommand('insertImage', false, dummyNodeId);
295
var dummyImageNodePattern = new RegExp('<[^<]*' + dummyNodeId + '[^>]*>');
296
var parent = this.getFieldObject().getRange().getContainerElement();
297
if (parent.nodeType == goog.dom.NodeType.TEXT) {
298
// Opera sometimes returns a text node here.
299
// TODO(user): perhaps we should modify getParentContainer?
300
parent = parent.parentNode;
301
}
302
303
// We have to search up the DOM because in some cases, notably when
304
// selecting li's within a list, execCommand('insertImage') actually splits
305
// tags in such a way that parent that used to contain the selection does
306
// not contain inserted image.
307
while (!dummyImageNodePattern.test(parent.innerHTML)) {
308
parent = parent.parentNode;
309
}
310
311
// Like the IE case above, sometimes the selection does not include the
312
// outermost tags. For Gecko, we have already expanded the range so that
313
// it does, so we can just replace the dummy image with the final html.
314
// For WebKit, we use the same approach as we do with IE - we
315
// inject a dummy span where we will eventually place the contents, and
316
// remove parentNodes of the span while they are empty.
317
318
if (goog.userAgent.GECKO) {
319
// Escape dollars passed in second argument of String.proto.replace.
320
// And since we're using that to replace, we need to escape those as well,
321
// hence the 2*2 dollar signs.
322
goog.editor.node.replaceInnerHtml(
323
parent, parent.innerHTML.replace(
324
dummyImageNodePattern, html.replace(/\$/g, '$$$$')));
325
} else {
326
goog.editor.node.replaceInnerHtml(
327
parent,
328
parent.innerHTML.replace(dummyImageNodePattern, dummySpanText));
329
var dummySpan = dh.getElement(dummyNodeId);
330
parent = dummySpan;
331
while ((parent = dummySpan.parentNode) &&
332
goog.editor.node.isEmpty(parent) &&
333
!goog.editor.node.isEditableContainer(parent)) {
334
var tag = parent.nodeName;
335
// We can't remove these table tags as it will invalidate the table dom.
336
if (tag == goog.dom.TagName.TD || tag == goog.dom.TagName.TR ||
337
tag == goog.dom.TagName.TH) {
338
break;
339
}
340
341
// We can't just remove parent since dummySpan is inside it, and we need
342
// to keep dummy span around for the replacement. So we move the
343
// dummySpan up as we go.
344
goog.dom.insertSiblingAfter(dummySpan, parent);
345
goog.dom.removeNode(parent);
346
}
347
goog.editor.node.replaceInnerHtml(
348
parent,
349
// Escape dollars passed in second argument of String.proto.replace
350
parent.innerHTML.replace(
351
new RegExp(dummySpanText, 'i'), html.replace(/\$/g, '$$$$')));
352
}
353
}
354
355
var startSpan = dh.getElement(startSpanId);
356
var endSpan = dh.getElement(endSpanId);
357
goog.dom.Range
358
.createFromNodes(startSpan, 0, endSpan, endSpan.childNodes.length)
359
.select();
360
goog.dom.removeNode(startSpan);
361
goog.dom.removeNode(endSpan);
362
};
363
364
365
/**
366
* Gets the html inside the selection to send off for further processing.
367
*
368
* TODO(user): Make this general so that it can be moved into
369
* goog.editor.range. The main reason it can't be moved is because we need to
370
* get the range before we do the execCommand and continue to operate on that
371
* same range (reasons are documented above).
372
*
373
* @param {goog.dom.AbstractRange} range The selection.
374
* @return {string} The html string to format.
375
* @private
376
*/
377
goog.editor.plugins.RemoveFormatting.prototype.getHtmlText_ = function(range) {
378
var div = this.getFieldDomHelper().createDom(goog.dom.TagName.DIV);
379
var textRange = range.getBrowserRangeObject();
380
381
if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
382
// Get the text to convert.
383
div.appendChild(textRange.cloneContents());
384
} else if (goog.editor.BrowserFeature.HAS_IE_RANGES) {
385
// Trim the whitespace on the ends of the range, so that it the container
386
// will be the container of only the text content that we are changing.
387
// This gets around issues in IE where the spaces are included in the
388
// selection, but ignored sometimes by execCommand, and left orphaned.
389
var rngText = range.getText();
390
391
// BRs get reported as \r\n, but only count as one character for moves.
392
// Adjust the string so our move counter is correct.
393
rngText = rngText.replace(/\r\n/g, '\r');
394
395
var rngTextLength = rngText.length;
396
var left = rngTextLength - goog.string.trimLeft(rngText).length;
397
var right = rngTextLength - goog.string.trimRight(rngText).length;
398
399
textRange.moveStart('character', left);
400
textRange.moveEnd('character', -right);
401
402
var htmlText = textRange.htmlText;
403
// Check if in pretag and fix up formatting so that new lines are preserved.
404
if (textRange.queryCommandValue('formatBlock') == 'Formatted') {
405
htmlText = goog.string.newLineToBr(textRange.htmlText);
406
}
407
div.innerHTML = htmlText;
408
}
409
410
// Get the innerHTML of the node instead of just returning the text above
411
// so that its properly html escaped.
412
return div.innerHTML;
413
};
414
415
416
/**
417
* Move the range so that it doesn't include any partially selected tables.
418
* @param {goog.dom.AbstractRange} range The range to adjust.
419
* @param {Node} startInTable Table node that the range starts in.
420
* @param {Node} endInTable Table node that the range ends in.
421
* @return {!goog.dom.SavedCaretRange} Range to use to restore the
422
* selection after we run our custom remove formatting.
423
* @private
424
*/
425
goog.editor.plugins.RemoveFormatting.prototype.adjustRangeForTables_ = function(
426
range, startInTable, endInTable) {
427
// Create placeholders for the current selection so we can restore it
428
// later.
429
var savedCaretRange = goog.editor.range.saveUsingNormalizedCarets(range);
430
431
var startNode = range.getStartNode();
432
var startOffset = range.getStartOffset();
433
var endNode = range.getEndNode();
434
var endOffset = range.getEndOffset();
435
var dh = this.getFieldDomHelper();
436
437
// Move start after the table.
438
if (startInTable) {
439
var textNode = dh.createTextNode('');
440
goog.dom.insertSiblingAfter(textNode, startInTable);
441
startNode = textNode;
442
startOffset = 0;
443
}
444
// Move end before the table.
445
if (endInTable) {
446
var textNode = dh.createTextNode('');
447
goog.dom.insertSiblingBefore(textNode, endInTable);
448
endNode = textNode;
449
endOffset = 0;
450
}
451
452
goog.dom.Range.createFromNodes(startNode, startOffset, endNode, endOffset)
453
.select();
454
455
return savedCaretRange;
456
};
457
458
459
/**
460
* Remove a caret from the dom and hide it in a safe place, so it can
461
* be restored later via restoreCaretsFromCave.
462
* @param {goog.dom.SavedCaretRange} caretRange The caret range to
463
* get the carets from.
464
* @param {boolean} isStart Whether this is the start or end caret.
465
* @private
466
*/
467
goog.editor.plugins.RemoveFormatting.prototype.putCaretInCave_ = function(
468
caretRange, isStart) {
469
var cavedCaret = goog.dom.removeNode(caretRange.getCaret(isStart));
470
if (isStart) {
471
this.startCaretInCave_ = cavedCaret;
472
} else {
473
this.endCaretInCave_ = cavedCaret;
474
}
475
};
476
477
478
/**
479
* Restore carets that were hidden away by adding them back into the dom.
480
* Note: this does not restore to the original dom location, as that
481
* will likely have been modified with remove formatting. The only
482
* guarantees here are that start will still be before end, and that
483
* they will be in the editable region. This should only be used when
484
* you don't actually intend to USE the caret again.
485
* @private
486
*/
487
goog.editor.plugins.RemoveFormatting.prototype.restoreCaretsFromCave_ =
488
function() {
489
// To keep start before end, we put the end caret at the bottom of the field
490
// and the start caret at the start of the field.
491
var field = this.getFieldObject().getElement();
492
if (this.startCaretInCave_) {
493
field.insertBefore(this.startCaretInCave_, field.firstChild);
494
this.startCaretInCave_ = null;
495
}
496
if (this.endCaretInCave_) {
497
field.appendChild(this.endCaretInCave_);
498
this.endCaretInCave_ = null;
499
}
500
};
501
502
503
/**
504
* Gets the html inside the current selection, passes it through the given
505
* conversion function, and puts it back into the selection.
506
*
507
* @param {function(string): string} convertFunc A conversion function that
508
* transforms an html string to new html string.
509
* @private
510
*/
511
goog.editor.plugins.RemoveFormatting.prototype.convertSelectedHtmlText_ =
512
function(convertFunc) {
513
var range = this.getFieldObject().getRange();
514
515
// For multiple ranges, it is really hard to do our custom remove formatting
516
// without invalidating other ranges. So instead of always losing the
517
// content, this solution at least lets the browser do its own remove
518
// formatting which works correctly most of the time.
519
if (range.getTextRangeCount() > 1) {
520
return;
521
}
522
523
if (goog.userAgent.GECKO || goog.userAgent.EDGE) {
524
// Determine if we need to handle tables, since they are special cases.
525
// If the selection is entirely within a table, there is no extra
526
// formatting removal we can do. If a table is fully selected, we will
527
// just blow it away. If a table is only partially selected, we can
528
// perform custom remove formatting only on the non table parts, since we
529
// we can't just remove the parts and paste back into it (eg. we can't
530
// inject html where a TR used to be).
531
// If the selection contains the table and more, this is automatically
532
// handled, but if just the table is selected, it can be tricky to figure
533
// this case out, because of the numerous ways selections can be formed -
534
// ex. if a table has a single tr with a single td with a single text node
535
// in it, and the selection is (textNode: 0), (textNode: nextNode.length)
536
// then the entire table is selected, even though the start and end aren't
537
// the table itself. We are truly inside a table if the expanded endpoints
538
// are still inside the table.
539
540
// Expand the selection to include any outermost tags that weren't included
541
// in the selection, but have the same visible selection. Stop expanding
542
// if we reach the top level field.
543
var expandedRange =
544
goog.editor.range.expand(range, this.getFieldObject().getElement());
545
546
var startInTable = this.getTableAncestor_(expandedRange.getStartNode());
547
var endInTable = this.getTableAncestor_(expandedRange.getEndNode());
548
549
if (startInTable || endInTable) {
550
if (startInTable == endInTable) {
551
// We are fully contained in the same table, there is no extra
552
// remove formatting that we can do, just return and run browser
553
// formatting only.
554
return;
555
}
556
557
// Adjust the range to not contain any partially selected tables, since
558
// we don't want to run our custom remove formatting on them.
559
var savedCaretRange =
560
this.adjustRangeForTables_(range, startInTable, endInTable);
561
562
// Hack alert!!
563
// If start is not in a table, then the saved caret will get sent out
564
// for uber remove formatting, and it will get blown away. This is
565
// fine, except that we need to be able to re-create a range from the
566
// savedCaretRange later on. So, we just remove it from the dom, and
567
// put it back later so we can create a range later (not exactly in the
568
// same spot, but don't worry we don't actually try to use it later)
569
// and then it will be removed when we dispose the range.
570
if (!startInTable) {
571
this.putCaretInCave_(savedCaretRange, true);
572
}
573
if (!endInTable) {
574
this.putCaretInCave_(savedCaretRange, false);
575
}
576
577
// Re-fetch the range, and re-expand it, since we just modified it.
578
range = this.getFieldObject().getRange();
579
expandedRange =
580
goog.editor.range.expand(range, this.getFieldObject().getElement());
581
}
582
583
expandedRange.select();
584
range = expandedRange;
585
}
586
587
// Convert the selected text to the format-less version, paste back into
588
// the selection.
589
var text = this.getHtmlText_(range);
590
this.pasteHtml_(convertFunc(text));
591
592
if ((goog.userAgent.GECKO || goog.userAgent.EDGE) && savedCaretRange) {
593
// If we moved the selection, move it back so the user can't tell we did
594
// anything crazy and so the browser removeFormat that we call next
595
// will operate on the entire originally selected range.
596
range = this.getFieldObject().getRange();
597
this.restoreCaretsFromCave_();
598
var realSavedCaretRange = savedCaretRange.toAbstractRange();
599
var startRange = startInTable ? realSavedCaretRange : range;
600
var endRange = endInTable ? realSavedCaretRange : range;
601
var restoredRange =
602
goog.editor.plugins.RemoveFormatting.createRangeDelimitedByRanges_(
603
startRange, endRange);
604
restoredRange.select();
605
savedCaretRange.dispose();
606
}
607
};
608
609
610
/**
611
* Does a best-effort attempt at clobbering all formatting that the
612
* browser's execCommand couldn't clobber without being totally inefficient.
613
* Attempts to convert visual line breaks to BRs. Leaves anchors that contain an
614
* href and images.
615
* Adapted from Gmail's MessageUtil's htmlToPlainText. http://go/messageutil.js
616
* @param {string} html The original html of the message.
617
* @return {string} The unformatted html, which is just text, br's, anchors and
618
* images.
619
* @private
620
*/
621
goog.editor.plugins.RemoveFormatting.prototype.removeFormattingWorker_ =
622
function(html) {
623
var el = goog.dom.createElement(goog.dom.TagName.DIV);
624
el.innerHTML = html;
625
626
// Put everything into a string buffer to avoid lots of expensive string
627
// concatenation along the way.
628
var sb = [];
629
var stack = [el.childNodes, 0];
630
631
// Keep separate stacks for places where we need to keep track of
632
// how deeply embedded we are. These are analogous to the general stack.
633
var preTagStack = [];
634
var preTagLevel = 0; // Length of the prestack.
635
var tableStack = [];
636
var tableLevel = 0;
637
638
// sp = stack pointer, pointing to the stack array.
639
// decrement by 2 since the stack alternates node lists and
640
// processed node counts
641
for (var sp = 0; sp >= 0; sp -= 2) {
642
// Check if we should pop the table level.
643
var changedLevel = false;
644
while (tableLevel > 0 && sp <= tableStack[tableLevel - 1]) {
645
tableLevel--;
646
changedLevel = true;
647
}
648
if (changedLevel) {
649
goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
650
}
651
652
653
// Check if we should pop the <pre>/<xmp> level.
654
changedLevel = false;
655
while (preTagLevel > 0 && sp <= preTagStack[preTagLevel - 1]) {
656
preTagLevel--;
657
changedLevel = true;
658
}
659
if (changedLevel) {
660
goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
661
}
662
663
// The list of of nodes to process at the current stack level.
664
var nodeList = stack[sp];
665
// The number of nodes processed so far, stored in the stack immediately
666
// following the node list for that stack level.
667
var numNodesProcessed = stack[sp + 1];
668
669
while (numNodesProcessed < nodeList.length) {
670
var node = nodeList[numNodesProcessed++];
671
var nodeName = node.nodeName;
672
673
var formatted = this.getValueForNode(node);
674
if (goog.isDefAndNotNull(formatted)) {
675
sb.push(formatted);
676
continue;
677
}
678
679
// TODO(user): Handle case 'EMBED' and case 'OBJECT'.
680
switch (nodeName) {
681
case '#text':
682
// Note that IE does not preserve whitespace in the dom
683
// values, even in a pre tag, so this is useless for IE.
684
var nodeValue = preTagLevel > 0 ?
685
node.nodeValue :
686
goog.string.stripNewlines(node.nodeValue);
687
nodeValue = goog.string.htmlEscape(nodeValue);
688
sb.push(nodeValue);
689
continue;
690
691
case String(goog.dom.TagName.P):
692
goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
693
goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
694
break; // break (not continue) so that child nodes are processed.
695
696
case String(goog.dom.TagName.BR):
697
goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
698
continue;
699
700
case String(goog.dom.TagName.TABLE):
701
goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
702
tableStack[tableLevel++] = sp;
703
break;
704
705
case String(goog.dom.TagName.PRE):
706
case 'XMP':
707
// This doesn't fully handle xmp, since
708
// it doesn't actually ignore tags within the xmp tag.
709
preTagStack[preTagLevel++] = sp;
710
break;
711
712
case String(goog.dom.TagName.STYLE):
713
case String(goog.dom.TagName.SCRIPT):
714
case String(goog.dom.TagName.SELECT):
715
continue;
716
717
case String(goog.dom.TagName.A):
718
if (node.href && node.href != '') {
719
sb.push("<a href='");
720
sb.push(node.href);
721
sb.push("'>");
722
sb.push(this.removeFormattingWorker_(node.innerHTML));
723
sb.push('</a>');
724
continue; // Children taken care of.
725
} else {
726
break; // Take care of the children.
727
}
728
729
case String(goog.dom.TagName.IMG):
730
sb.push("<img src='");
731
sb.push(node.src);
732
sb.push("'");
733
// border=0 is a common way to not show a blue border around an image
734
// that is wrapped by a link. If we remove that, the blue border will
735
// show up, which to the user looks like adding format, not removing.
736
if (node.border == '0') {
737
sb.push(" border='0'");
738
}
739
sb.push('>');
740
continue;
741
742
case String(goog.dom.TagName.TD):
743
// Don't add a space for the first TD, we only want spaces to
744
// separate td's.
745
if (node.previousSibling) {
746
sb.push(' ');
747
}
748
break;
749
750
case String(goog.dom.TagName.TR):
751
// Don't add a newline for the first TR.
752
if (node.previousSibling) {
753
goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
754
}
755
break;
756
757
case String(goog.dom.TagName.DIV):
758
var parent = node.parentNode;
759
if (parent.firstChild == node &&
760
goog.editor.plugins.RemoveFormatting.BLOCK_RE_.test(
761
parent.tagName)) {
762
// If a DIV is the first child of another element that itself is a
763
// block element, the DIV does not add a new line.
764
break;
765
}
766
// Otherwise, the DIV does add a new line. Fall through.
767
768
default:
769
if (goog.editor.plugins.RemoveFormatting.BLOCK_RE_.test(nodeName)) {
770
goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
771
}
772
}
773
774
// Recurse down the node.
775
var children = node.childNodes;
776
if (children.length > 0) {
777
// Push the current state on the stack.
778
stack[sp++] = nodeList;
779
stack[sp++] = numNodesProcessed;
780
781
// Iterate through the children nodes.
782
nodeList = children;
783
numNodesProcessed = 0;
784
}
785
}
786
}
787
788
// Replace &nbsp; with white space.
789
return goog.string.normalizeSpaces(sb.join(''));
790
};
791
792
793
/**
794
* Handle per node special processing if necessary. If this function returns
795
* null then standard cleanup is applied. Otherwise this node and all children
796
* are assumed to be cleaned.
797
* NOTE(user): If an alternate RemoveFormatting processor is provided
798
* (setRemoveFormattingFunc()), this will no longer work.
799
* @param {Element} node The node to clean.
800
* @return {?string} The HTML strig representation of the cleaned data.
801
*/
802
goog.editor.plugins.RemoveFormatting.prototype.getValueForNode = function(
803
node) {
804
return null;
805
};
806
807
808
/**
809
* Sets a function to be used for remove formatting.
810
* @param {function(string): string} removeFormattingFunc - A function that
811
* takes a string of html and returns a string of html that does any other
812
* formatting changes desired. Use this only if trogedit's behavior doesn't
813
* meet your needs.
814
*/
815
goog.editor.plugins.RemoveFormatting.prototype.setRemoveFormattingFunc =
816
function(removeFormattingFunc) {
817
this.optRemoveFormattingFunc_ = removeFormattingFunc;
818
};
819
820