]> git.immae.eu Git - perso/Immae/Projets/packagist/ludivine-ckeditor-component.git/blob - sources/core/htmldataprocessor.js
Update to 4.7.3
[perso/Immae/Projets/packagist/ludivine-ckeditor-component.git] / sources / core / htmldataprocessor.js
1 /**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6 ( function() {
7 /**
8 * Represents an HTML data processor, which is responsible for translating and
9 * transforming the editor data on input and output.
10 *
11 * @class
12 * @extends CKEDITOR.dataProcessor
13 * @constructor Creates an htmlDataProcessor class instance.
14 * @param {CKEDITOR.editor} editor
15 */
16 CKEDITOR.htmlDataProcessor = function( editor ) {
17 var dataFilter, htmlFilter,
18 that = this;
19
20 this.editor = editor;
21
22 /**
23 * Data filter used when processing input by {@link #toHtml}.
24 *
25 * @property {CKEDITOR.htmlParser.filter}
26 */
27 this.dataFilter = dataFilter = new CKEDITOR.htmlParser.filter();
28
29 /**
30 * HTML filter used when processing output by {@link #toDataFormat}.
31 *
32 * @property {CKEDITOR.htmlParser.filter}
33 */
34 this.htmlFilter = htmlFilter = new CKEDITOR.htmlParser.filter();
35
36 /**
37 * The HTML writer used by this data processor to format the output.
38 *
39 * @property {CKEDITOR.htmlParser.basicWriter}
40 */
41 this.writer = new CKEDITOR.htmlParser.basicWriter();
42
43 dataFilter.addRules( defaultDataFilterRulesEditableOnly );
44 dataFilter.addRules( defaultDataFilterRulesForAll, { applyToAll: true } );
45 dataFilter.addRules( createBogusAndFillerRules( editor, 'data' ), { applyToAll: true } );
46 htmlFilter.addRules( defaultHtmlFilterRulesEditableOnly );
47 htmlFilter.addRules( defaultHtmlFilterRulesForAll, { applyToAll: true } );
48 htmlFilter.addRules( createBogusAndFillerRules( editor, 'html' ), { applyToAll: true } );
49
50 editor.on( 'toHtml', function( evt ) {
51 var evtData = evt.data,
52 data = evtData.dataValue,
53 fixBodyTag;
54
55 // The source data is already HTML, but we need to clean
56 // it up and apply the filter.
57 data = protectSource( data, editor );
58
59 // Protect content of textareas. (http://dev.ckeditor.com/ticket/9995)
60 // Do this before protecting attributes to avoid breaking:
61 // <textarea><img src="..." /></textarea>
62 data = protectElements( data, protectTextareaRegex );
63
64 // Before anything, we must protect the URL attributes as the
65 // browser may changing them when setting the innerHTML later in
66 // the code.
67 data = protectAttributes( data );
68
69 // Protect elements than can't be set inside a DIV. E.g. IE removes
70 // style tags from innerHTML. (http://dev.ckeditor.com/ticket/3710)
71 data = protectElements( data, protectElementsRegex );
72
73 // Certain elements has problem to go through DOM operation, protect
74 // them by prefixing 'cke' namespace. (http://dev.ckeditor.com/ticket/3591)
75 data = protectElementsNames( data );
76
77 // All none-IE browsers ignore self-closed custom elements,
78 // protecting them into open-close. (http://dev.ckeditor.com/ticket/3591)
79 data = protectSelfClosingElements( data );
80
81 // Compensate one leading line break after <pre> open as browsers
82 // eat it up. (http://dev.ckeditor.com/ticket/5789)
83 data = protectPreFormatted( data );
84
85 // There are attributes which may execute JavaScript code inside fixBin.
86 // Encode them greedily. They will be unprotected right after getting HTML from fixBin. (http://dev.ckeditor.com/ticket/10)
87 data = protectInsecureAttributes( data );
88
89 var fixBin = evtData.context || editor.editable().getName(),
90 isPre;
91
92 // Old IEs loose formats when load html into <pre>.
93 if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 && fixBin == 'pre' ) {
94 fixBin = 'div';
95 data = '<pre>' + data + '</pre>';
96 isPre = 1;
97 }
98
99 // Call the browser to help us fixing a possibly invalid HTML
100 // structure.
101 var el = editor.document.createElement( fixBin );
102 // Add fake character to workaround IE comments bug. (http://dev.ckeditor.com/ticket/3801)
103 el.setHtml( 'a' + data );
104 data = el.getHtml().substr( 1 );
105
106 // Restore shortly protected attribute names.
107 data = data.replace( new RegExp( 'data-cke-' + CKEDITOR.rnd + '-', 'ig' ), '' );
108
109 isPre && ( data = data.replace( /^<pre>|<\/pre>$/gi, '' ) );
110
111 // Unprotect "some" of the protected elements at this point.
112 data = unprotectElementNames( data );
113
114 data = unprotectElements( data );
115
116 // Restore the comments that have been protected, in this way they
117 // can be properly filtered.
118 data = unprotectRealComments( data );
119
120 if ( evtData.fixForBody === false ) {
121 fixBodyTag = false;
122 } else {
123 fixBodyTag = getFixBodyTag( evtData.enterMode, editor.config.autoParagraph );
124 }
125
126 // Now use our parser to make further fixes to the structure, as
127 // well as apply the filter.
128 data = CKEDITOR.htmlParser.fragment.fromHtml( data, evtData.context, fixBodyTag );
129
130 // The empty root element needs to be fixed by adding 'p' or 'div' into it.
131 // This avoids the need to create that element on the first focus (http://dev.ckeditor.com/ticket/12630).
132 if ( fixBodyTag ) {
133 fixEmptyRoot( data, fixBodyTag );
134 }
135
136 evtData.dataValue = data;
137 }, null, null, 5 );
138
139 // Filter incoming "data".
140 // Add element filter before htmlDataProcessor.dataFilter when purifying input data to correct html.
141 editor.on( 'toHtml', function( evt ) {
142 if ( evt.data.filter.applyTo( evt.data.dataValue, true, evt.data.dontFilter, evt.data.enterMode ) )
143 editor.fire( 'dataFiltered' );
144 }, null, null, 6 );
145
146 editor.on( 'toHtml', function( evt ) {
147 evt.data.dataValue.filterChildren( that.dataFilter, true );
148 }, null, null, 10 );
149
150 editor.on( 'toHtml', function( evt ) {
151 var evtData = evt.data,
152 data = evtData.dataValue,
153 writer = new CKEDITOR.htmlParser.basicWriter();
154
155 data.writeChildrenHtml( writer );
156 data = writer.getHtml( true );
157
158 // Protect the real comments again.
159 evtData.dataValue = protectRealComments( data );
160 }, null, null, 15 );
161
162
163 editor.on( 'toDataFormat', function( evt ) {
164 var data = evt.data.dataValue;
165
166 // http://dev.ckeditor.com/ticket/10854 - we need to strip leading blockless <br> which FF adds
167 // automatically when editable contains only non-editable content.
168 // We do that for every browser (so it's a constant behavior) and
169 // not in BR mode, in which chance of valid leading blockless <br> is higher.
170 if ( evt.data.enterMode != CKEDITOR.ENTER_BR )
171 data = data.replace( /^<br *\/?>/i, '' );
172
173 evt.data.dataValue = CKEDITOR.htmlParser.fragment.fromHtml(
174 data, evt.data.context, getFixBodyTag( evt.data.enterMode, editor.config.autoParagraph ) );
175 }, null, null, 5 );
176
177 editor.on( 'toDataFormat', function( evt ) {
178 evt.data.dataValue.filterChildren( that.htmlFilter, true );
179 }, null, null, 10 );
180
181 // Transform outcoming "data".
182 // Add element filter after htmlDataProcessor.htmlFilter when preparing output data HTML.
183 editor.on( 'toDataFormat', function( evt ) {
184 evt.data.filter.applyTo( evt.data.dataValue, false, true );
185 }, null, null, 11 );
186
187 editor.on( 'toDataFormat', function( evt ) {
188 var data = evt.data.dataValue,
189 writer = that.writer;
190
191 writer.reset();
192 data.writeChildrenHtml( writer );
193 data = writer.getHtml( true );
194
195 // Restore those non-HTML protected source. (http://dev.ckeditor.com/ticket/4475,http://dev.ckeditor.com/ticket/4880)
196 data = unprotectRealComments( data );
197 data = unprotectSource( data, editor );
198
199 evt.data.dataValue = data;
200 }, null, null, 15 );
201 };
202
203 CKEDITOR.htmlDataProcessor.prototype = {
204 /**
205 * Processes the (potentially malformed) input HTML to a purified form which
206 * is suitable for using in the WYSIWYG editable.
207 *
208 * This method fires the {@link CKEDITOR.editor#toHtml} event which makes it possible
209 * to hook into the process at various stages.
210 *
211 * **Note:** Since CKEditor 4.3 the signature of this method changed and all options
212 * are now grouped in one `options` object. Previously `context`, `fixForBody` and `dontFilter`
213 * were passed separately.
214 *
215 * @param {String} data The raw data.
216 * @param {Object} [options] The options object.
217 * @param {String} [options.context] The tag name of a context element within which
218 * the input is to be processed, defaults to the editable element.
219 * If `null` is passed, then data will be parsed without context (as children of {@link CKEDITOR.htmlParser.fragment}).
220 * See {@link CKEDITOR.htmlParser.fragment#fromHtml} for more details.
221 * @param {Boolean} [options.fixForBody=true] Whether to trigger the auto paragraph for non-block content.
222 * @param {CKEDITOR.filter} [options.filter] When specified, instead of using the {@link CKEDITOR.editor#filter main filter},
223 * the passed instance will be used to filter the content.
224 * @param {Boolean} [options.dontFilter] Do not filter data with {@link CKEDITOR.filter} (note: transformations
225 * will still be applied).
226 * @param {Number} [options.enterMode] When specified, it will be used instead of the {@link CKEDITOR.editor#enterMode main enterMode}.
227 * @param {Boolean} [options.protectedWhitespaces] Indicates that content was wrapped with `<span>` elements to preserve
228 * leading and trailing whitespaces. Option used by the {@link CKEDITOR.editor#method-insertHtml} method.
229 * @returns {String}
230 */
231 toHtml: function( data, options, fixForBody, dontFilter ) {
232 var editor = this.editor,
233 context, filter, enterMode, protectedWhitespaces;
234
235 // Typeof null == 'object', so check truthiness of options too.
236 if ( options && typeof options == 'object' ) {
237 context = options.context;
238 fixForBody = options.fixForBody;
239 dontFilter = options.dontFilter;
240 filter = options.filter;
241 enterMode = options.enterMode;
242 protectedWhitespaces = options.protectedWhitespaces;
243 }
244 // Backward compatibility. Since CKEDITOR 4.3 every option was a separate argument.
245 else {
246 context = options;
247 }
248
249 // Fall back to the editable as context if not specified.
250 if ( !context && context !== null )
251 context = editor.editable().getName();
252
253 return editor.fire( 'toHtml', {
254 dataValue: data,
255 context: context,
256 fixForBody: fixForBody,
257 dontFilter: dontFilter,
258 filter: filter || editor.filter,
259 enterMode: enterMode || editor.enterMode,
260 protectedWhitespaces: protectedWhitespaces
261 } ).dataValue;
262 },
263
264 /**
265 * See {@link CKEDITOR.dataProcessor#toDataFormat}.
266 *
267 * This method fires the {@link CKEDITOR.editor#toDataFormat} event which makes it possible
268 * to hook into the process at various stages.
269 *
270 * @param {String} html
271 * @param {Object} [options] The options object.
272 * @param {String} [options.context] The tag name of the context element within which
273 * the input is to be processed, defaults to the editable element.
274 * @param {CKEDITOR.filter} [options.filter] When specified, instead of using the {@link CKEDITOR.editor#filter main filter},
275 * the passed instance will be used to apply content transformations to the content.
276 * @param {Number} [options.enterMode] When specified, it will be used instead of the {@link CKEDITOR.editor#enterMode main enterMode}.
277 * @returns {String}
278 */
279 toDataFormat: function( html, options ) {
280 var context, filter, enterMode;
281
282 // Do not shorten this to `options && options.xxx`, because
283 // falsy `options` will be passed instead of undefined.
284 if ( options ) {
285 context = options.context;
286 filter = options.filter;
287 enterMode = options.enterMode;
288 }
289
290 // Fall back to the editable as context if not specified.
291 if ( !context && context !== null )
292 context = this.editor.editable().getName();
293
294 return this.editor.fire( 'toDataFormat', {
295 dataValue: html,
296 filter: filter || this.editor.filter,
297 context: context,
298 enterMode: enterMode || this.editor.enterMode
299 } ).dataValue;
300 }
301 };
302
303 // Produce a set of filtering rules that handles bogus and filler node at the
304 // end of block/pseudo block, in the following consequence:
305 // 1. elements:<block> - this filter removes any bogus node, then check
306 // if it's an empty block that requires a filler.
307 // 2. elements:<br> - After cleaned with bogus, this filter checks the real
308 // line-break BR to compensate a filler after it.
309 //
310 // Terms definitions:
311 // filler: An element that's either <BR> or &NBSP; at the end of block that established line height.
312 // bogus: Whenever a filler is proceeded with inline content, it becomes a bogus which is subjected to be removed.
313 //
314 // Various forms of the filler:
315 // In output HTML: Filler should be consistently &NBSP; <BR> at the end of block is always considered as bogus.
316 // In Wysiwyg HTML: Browser dependent - see env.needsBrFiller. Either BR for when needsBrFiller is true, or &NBSP; otherwise.
317 // <BR> is NEVER considered as bogus when needsBrFiller is true.
318 function createBogusAndFillerRules( editor, type ) {
319 function createFiller( isOutput ) {
320 return isOutput || CKEDITOR.env.needsNbspFiller ?
321 new CKEDITOR.htmlParser.text( '\xa0' ) :
322 new CKEDITOR.htmlParser.element( 'br', { 'data-cke-bogus': 1 } );
323 }
324
325 // This text block filter, remove any bogus and create the filler on demand.
326 function blockFilter( isOutput, fillEmptyBlock ) {
327
328 return function( block ) {
329 // DO NOT apply the filler if it's a fragment node.
330 if ( block.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT )
331 return;
332
333 cleanBogus( block );
334
335 // Add fillers to input (always) and to output (if fillEmptyBlock is ok with that).
336 var shouldFillBlock = !isOutput ||
337 ( typeof fillEmptyBlock == 'function' ? fillEmptyBlock( block ) : fillEmptyBlock ) !== false;
338
339 if ( shouldFillBlock && isEmptyBlockNeedFiller( block ) ) {
340 block.add( createFiller( isOutput ) );
341 }
342 };
343 }
344
345 // Append a filler right after the last line-break BR, found at the end of block.
346 function brFilter( isOutput ) {
347 return function( br ) {
348 // DO NOT apply the filer if parent's a fragment node.
349 if ( br.parent.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT )
350 return;
351
352 var attrs = br.attributes;
353 // Dismiss BRs that are either bogus or eol marker.
354 if ( 'data-cke-bogus' in attrs || 'data-cke-eol' in attrs ) {
355 delete attrs [ 'data-cke-bogus' ];
356 return;
357 }
358
359 // Judge the tail line-break BR, and to insert bogus after it.
360 var next = getNext( br ), previous = getPrevious( br );
361
362 if ( !next && isBlockBoundary( br.parent ) )
363 append( br.parent, createFiller( isOutput ) );
364 else if ( isBlockBoundary( next ) && previous && !isBlockBoundary( previous ) )
365 createFiller( isOutput ).insertBefore( next );
366 };
367 }
368
369 // Determinate whether this node is potentially a bogus node.
370 function maybeBogus( node, atBlockEnd ) {
371
372 // BR that's not from IE<11 DOM, except for a EOL marker.
373 if ( !( isOutput && !CKEDITOR.env.needsBrFiller ) &&
374 node.type == CKEDITOR.NODE_ELEMENT && node.name == 'br' &&
375 !node.attributes[ 'data-cke-eol' ] ) {
376 return true;
377 }
378
379 var match;
380
381 // NBSP, possibly.
382 if ( node.type == CKEDITOR.NODE_TEXT && ( match = node.value.match( tailNbspRegex ) ) ) {
383 // We need to separate tail NBSP out of a text node, for later removal.
384 if ( match.index ) {
385 ( new CKEDITOR.htmlParser.text( node.value.substring( 0, match.index ) ) ).insertBefore( node );
386 node.value = match[ 0 ];
387 }
388
389 // From IE<11 DOM, at the end of a text block, or before block boundary.
390 if ( !CKEDITOR.env.needsBrFiller && isOutput && ( !atBlockEnd || node.parent.name in textBlockTags ) )
391 return true;
392
393 // From the output.
394 if ( !isOutput ) {
395 var previous = node.previous;
396
397 // Following a line-break at the end of block.
398 if ( previous && previous.name == 'br' )
399 return true;
400
401 // Or a single NBSP between two blocks.
402 if ( !previous || isBlockBoundary( previous ) )
403 return true;
404 }
405 }
406
407 return false;
408 }
409
410 // Removes all bogus inside of this block, and to convert fillers into the proper form.
411 function cleanBogus( block ) {
412 var bogus = [];
413 var last = getLast( block ), node, previous;
414
415 if ( last ) {
416 // Check for bogus at the end of this block.
417 // e.g. <p>foo<br /></p>
418 maybeBogus( last, 1 ) && bogus.push( last );
419
420 while ( last ) {
421 // Check for bogus at the end of any pseudo block contained.
422 if ( isBlockBoundary( last ) && ( node = getPrevious( last ) ) && maybeBogus( node ) ) {
423 // Bogus must have inline proceeding, instead single BR between two blocks,
424 // is considered as filler, e.g. <hr /><br /><hr />
425 if ( ( previous = getPrevious( node ) ) && !isBlockBoundary( previous ) )
426 bogus.push( node );
427 // Convert the filler into appropriate form.
428 else {
429 createFiller( isOutput ).insertAfter( node );
430 node.remove();
431 }
432 }
433
434 last = last.previous;
435 }
436 }
437
438 // Now remove all bogus collected from above.
439 for ( var i = 0 ; i < bogus.length ; i++ )
440 bogus[ i ].remove();
441 }
442
443 // Judge whether it's an empty block that requires a filler node.
444 function isEmptyBlockNeedFiller( block ) {
445
446 // DO NOT fill empty editable in IE<11.
447 if ( !isOutput && !CKEDITOR.env.needsBrFiller && block.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT )
448 return false;
449
450 // 1. For IE version >=8, empty blocks are displayed correctly themself in wysiwiyg;
451 // 2. For the rest, at least table cell and list item need no filler space. (http://dev.ckeditor.com/ticket/6248)
452 if ( !isOutput && !CKEDITOR.env.needsBrFiller &&
453 ( document.documentMode > 7 ||
454 block.name in CKEDITOR.dtd.tr ||
455 block.name in CKEDITOR.dtd.$listItem ) ) {
456 return false;
457 }
458
459 var last = getLast( block );
460 return !last || block.name == 'form' && last.name == 'input' ;
461 }
462
463 var rules = { elements: {} },
464 isOutput = type == 'html',
465 textBlockTags = CKEDITOR.tools.extend( {}, blockLikeTags );
466
467 // Build the list of text blocks.
468 for ( var i in textBlockTags ) {
469 if ( !( '#' in dtd[ i ] ) )
470 delete textBlockTags[ i ];
471 }
472
473 for ( i in textBlockTags )
474 rules.elements[ i ] = blockFilter( isOutput, editor.config.fillEmptyBlocks );
475
476 // Editable element has to be checked separately.
477 rules.root = blockFilter( isOutput, false );
478 rules.elements.br = brFilter( isOutput );
479 return rules;
480 }
481
482 function getFixBodyTag( enterMode, autoParagraph ) {
483 return ( enterMode != CKEDITOR.ENTER_BR && autoParagraph !== false ) ? enterMode == CKEDITOR.ENTER_DIV ? 'div' : 'p' : false;
484 }
485
486 // Regex to scan for &nbsp; at the end of blocks, which are actually placeholders.
487 // Safari transforms the &nbsp; to \xa0. (http://dev.ckeditor.com/ticket/4172)
488 var tailNbspRegex = /(?:&nbsp;|\xa0)$/;
489
490 var protectedSourceMarker = '{cke_protected}';
491
492 function getLast( node ) {
493 var last = node.children[ node.children.length - 1 ];
494 while ( last && isEmpty( last ) )
495 last = last.previous;
496 return last;
497 }
498
499 function getNext( node ) {
500 var next = node.next;
501 while ( next && isEmpty( next ) )
502 next = next.next;
503 return next;
504 }
505
506 function getPrevious( node ) {
507 var previous = node.previous;
508 while ( previous && isEmpty( previous ) )
509 previous = previous.previous;
510 return previous;
511 }
512
513 // Judge whether the node is an ghost node to be ignored, when traversing.
514 function isEmpty( node ) {
515 return node.type == CKEDITOR.NODE_TEXT &&
516 !CKEDITOR.tools.trim( node.value ) ||
517 node.type == CKEDITOR.NODE_ELEMENT &&
518 node.attributes[ 'data-cke-bookmark' ];
519 }
520
521 // Judge whether the node is a block-like element.
522 function isBlockBoundary( node ) {
523 return node &&
524 ( node.type == CKEDITOR.NODE_ELEMENT && node.name in blockLikeTags ||
525 node.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT );
526 }
527
528 function append( parent, node ) {
529 var last = parent.children[ parent.children.length - 1 ];
530 parent.children.push( node );
531 node.parent = parent;
532 if ( last ) {
533 last.next = node;
534 node.previous = last;
535 }
536 }
537
538 function getNodeIndex( node ) {
539 return node.parent ? node.getIndex() : -1;
540 }
541
542 var dtd = CKEDITOR.dtd,
543 // Define orders of table elements.
544 tableOrder = [ 'caption', 'colgroup', 'col', 'thead', 'tfoot', 'tbody' ],
545 // List of all block elements.
546 blockLikeTags = CKEDITOR.tools.extend( {}, dtd.$blockLimit, dtd.$block );
547
548 //
549 // DATA filter rules ------------------------------------------------------
550 //
551
552 var defaultDataFilterRulesEditableOnly = {
553 elements: {
554 input: protectReadOnly,
555 textarea: protectReadOnly
556 }
557 };
558
559 // These rules will also be applied to non-editable content.
560 var defaultDataFilterRulesForAll = {
561 attributeNames: [
562 // Event attributes (onXYZ) must not be directly set. They can become
563 // active in the editing area (IE|WebKit).
564 [ ( /^on/ ), 'data-cke-pa-on' ],
565
566 // Prevent iframe's srcdoc attribute from being evaluated in the editable.
567 [ ( /^srcdoc/ ), 'data-cke-pa-srcdoc' ],
568
569 // Don't let some old expando enter editor. Concerns only IE8,
570 // but for consistency remove on all browsers.
571 [ ( /^data-cke-expando$/ ), '' ]
572 ],
573
574 elements: {
575 // Prevent iframe's src attribute with javascript code or data protocol from being evaluated in the editable.
576 iframe: function( element ) {
577 if ( element.attributes && element.attributes.src ) {
578
579 var src = element.attributes.src.toLowerCase().replace( /[^a-z]/gi, '' );
580 if ( src.indexOf( 'javascript' ) === 0 || src.indexOf( 'data' ) === 0 ) {
581 element.attributes[ 'data-cke-pa-src' ] = element.attributes.src;
582 delete element.attributes.src;
583 }
584 }
585 }
586 }
587 };
588
589 // Disable form elements editing mode provided by some browsers. (http://dev.ckeditor.com/ticket/5746)
590 function protectReadOnly( element ) {
591 var attrs = element.attributes;
592
593 // We should flag that the element was locked by our code so
594 // it'll be editable by the editor functions (http://dev.ckeditor.com/ticket/6046).
595 if ( attrs.contenteditable != 'false' )
596 attrs[ 'data-cke-editable' ] = attrs.contenteditable ? 'true' : 1;
597
598 attrs.contenteditable = 'false';
599 }
600
601 //
602 // HTML filter rules ------------------------------------------------------
603 //
604
605 var defaultHtmlFilterRulesEditableOnly = {
606 elements: {
607 embed: function( element ) {
608 var parent = element.parent;
609
610 // If the <embed> is child of a <object>, copy the width
611 // and height attributes from it.
612 if ( parent && parent.name == 'object' ) {
613 var parentWidth = parent.attributes.width,
614 parentHeight = parent.attributes.height;
615 if ( parentWidth )
616 element.attributes.width = parentWidth;
617 if ( parentHeight )
618 element.attributes.height = parentHeight;
619 }
620 },
621
622 // Remove empty link but not empty anchor. (http://dev.ckeditor.com/ticket/3829, http://dev.ckeditor.com/ticket/13516)
623 a: function( element ) {
624 var attrs = element.attributes;
625
626 if ( !( element.children.length || attrs.name || attrs.id || element.attributes[ 'data-cke-saved-name' ] ) )
627 return false;
628 }
629 }
630 };
631
632 // These rules will also be applied to non-editable content.
633 var defaultHtmlFilterRulesForAll = {
634 elementNames: [
635 // Remove the "cke:" namespace prefix.
636 [ ( /^cke:/ ), '' ],
637
638 // Ignore <?xml:namespace> tags.
639 [ ( /^\?xml:namespace$/ ), '' ]
640 ],
641
642 attributeNames: [
643 // Attributes saved for changes and protected attributes.
644 [ ( /^data-cke-(saved|pa)-/ ), '' ],
645
646 // All "data-cke-" attributes are to be ignored.
647 [ ( /^data-cke-.*/ ), '' ],
648
649 [ 'hidefocus', '' ]
650 ],
651
652 elements: {
653 $: function( element ) {
654 var attribs = element.attributes;
655
656 if ( attribs ) {
657 // Elements marked as temporary are to be ignored.
658 if ( attribs[ 'data-cke-temp' ] )
659 return false;
660
661 // Remove duplicated attributes - http://dev.ckeditor.com/ticket/3789.
662 var attributeNames = [ 'name', 'href', 'src' ],
663 savedAttributeName;
664 for ( var i = 0; i < attributeNames.length; i++ ) {
665 savedAttributeName = 'data-cke-saved-' + attributeNames[ i ];
666 savedAttributeName in attribs && ( delete attribs[ attributeNames[ i ] ] );
667 }
668 }
669
670 return element;
671 },
672
673 // The contents of table should be in correct order (http://dev.ckeditor.com/ticket/4809).
674 table: function( element ) {
675 // Clone the array as it would become empty during the sort call.
676 var children = element.children.slice( 0 );
677
678 children.sort( function( node1, node2 ) {
679 var index1, index2;
680
681 // Compare in the predefined order.
682 if ( node1.type == CKEDITOR.NODE_ELEMENT && node2.type == node1.type ) {
683 index1 = CKEDITOR.tools.indexOf( tableOrder, node1.name );
684 index2 = CKEDITOR.tools.indexOf( tableOrder, node2.name );
685 }
686
687 // Make sure the sort is stable, if no order can be established above.
688 if ( !( index1 > -1 && index2 > -1 && index1 != index2 ) ) {
689 index1 = getNodeIndex( node1 );
690 index2 = getNodeIndex( node2 );
691 }
692
693 return index1 > index2 ? 1 : -1;
694 } );
695 },
696
697 // Restore param elements into self-closing.
698 param: function( param ) {
699 param.children = [];
700 param.isEmpty = true;
701 return param;
702 },
703
704 // Remove dummy span in webkit.
705 span: function( element ) {
706 if ( element.attributes[ 'class' ] == 'Apple-style-span' )
707 delete element.name;
708 },
709
710 html: function( element ) {
711 delete element.attributes.contenteditable;
712 delete element.attributes[ 'class' ];
713 },
714
715 body: function( element ) {
716 delete element.attributes.spellcheck;
717 delete element.attributes.contenteditable;
718 },
719
720 style: function( element ) {
721 var child = element.children[ 0 ];
722 if ( child && child.value )
723 child.value = CKEDITOR.tools.trim( child.value );
724
725 if ( !element.attributes.type )
726 element.attributes.type = 'text/css';
727 },
728
729 title: function( element ) {
730 var titleText = element.children[ 0 ];
731
732 // Append text-node to title tag if not present (i.e. non-IEs) (http://dev.ckeditor.com/ticket/9882).
733 !titleText && append( element, titleText = new CKEDITOR.htmlParser.text() );
734
735 // Transfer data-saved title to title tag.
736 titleText.value = element.attributes[ 'data-cke-title' ] || '';
737 },
738
739 input: unprotectReadyOnly,
740 textarea: unprotectReadyOnly
741 },
742
743 attributes: {
744 'class': function( value ) {
745 // Remove all class names starting with "cke_".
746 return CKEDITOR.tools.ltrim( value.replace( /(?:^|\s+)cke_[^\s]*/g, '' ) ) || false;
747 }
748 }
749 };
750
751 if ( CKEDITOR.env.ie ) {
752 // IE outputs style attribute in capital letters. We should convert
753 // them back to lower case, while not hurting the values (http://dev.ckeditor.com/ticket/5930)
754 defaultHtmlFilterRulesForAll.attributes.style = function( value ) {
755 return value.replace( /(^|;)([^\:]+)/g, function( match ) {
756 return match.toLowerCase();
757 } );
758 };
759 }
760
761 // Disable form elements editing mode provided by some browsers. (http://dev.ckeditor.com/ticket/5746)
762 function unprotectReadyOnly( element ) {
763 var attrs = element.attributes;
764 switch ( attrs[ 'data-cke-editable' ] ) {
765 case 'true':
766 attrs.contenteditable = 'true';
767 break;
768 case '1':
769 delete attrs.contenteditable;
770 break;
771 }
772 }
773
774 //
775 // Preprocessor filters ---------------------------------------------------
776 //
777
778 var protectElementRegex = /<(a|area|img|input|source)\b([^>]*)>/gi,
779 // Be greedy while looking for protected attributes. This will let us avoid an unfortunate
780 // situation when "nested attributes", which may appear valid, are also protected.
781 // I.e. if we consider the following HTML:
782 //
783 // <img data-x="&lt;a href=&quot;X&quot;" />
784 //
785 // then the "non-greedy match" returns:
786 //
787 // 'href' => '&quot;X&quot;' // It's wrong! Href is not an attribute of <img>.
788 //
789 // while greedy match returns:
790 //
791 // 'data-x' => '&lt;a href=&quot;X&quot;'
792 //
793 // which, can be easily filtered out (http://dev.ckeditor.com/ticket/11508).
794 protectAttributeRegex = /([\w-:]+)\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|(?:[^ "'>]+))/gi,
795 protectAttributeNameRegex = /^(href|src|name)$/i;
796
797 // Note: we use lazy star '*?' to prevent eating everything up to the last occurrence of </style> or </textarea>.
798 var protectElementsRegex = /(?:<style(?=[ >])[^>]*>[\s\S]*?<\/style>)|(?:<(:?link|meta|base)[^>]*>)/gi,
799 protectTextareaRegex = /(<textarea(?=[ >])[^>]*>)([\s\S]*?)(?:<\/textarea>)/gi,
800 encodedElementsRegex = /<cke:encoded>([^<]*)<\/cke:encoded>/gi;
801
802 var protectElementNamesRegex = /(<\/?)((?:object|embed|param|html|body|head|title)[^>]*>)/gi,
803 unprotectElementNamesRegex = /(<\/?)cke:((?:html|body|head|title)[^>]*>)/gi;
804
805 var protectSelfClosingRegex = /<cke:(param|embed)([^>]*?)\/?>(?!\s*<\/cke:\1)/gi;
806
807 function protectAttributes( html ) {
808 return html.replace( protectElementRegex, function( element, tag, attributes ) {
809 return '<' + tag + attributes.replace( protectAttributeRegex, function( fullAttr, attrName ) {
810 // Avoid corrupting the inline event attributes (http://dev.ckeditor.com/ticket/7243).
811 // We should not rewrite the existed protected attributes, e.g. clipboard content from editor. (http://dev.ckeditor.com/ticket/5218)
812 if ( protectAttributeNameRegex.test( attrName ) && attributes.indexOf( 'data-cke-saved-' + attrName ) == -1 )
813 return ' data-cke-saved-' + fullAttr + ' data-cke-' + CKEDITOR.rnd + '-' + fullAttr;
814
815 return fullAttr;
816 } ) + '>';
817 } );
818 }
819
820 function protectElements( html, regex ) {
821 return html.replace( regex, function( match, tag, content ) {
822 // Encode < and > in textarea because this won't be done by a browser, since
823 // textarea will be protected during passing data through fix bin.
824 if ( match.indexOf( '<textarea' ) === 0 )
825 match = tag + unprotectRealComments( content ).replace( /</g, '&lt;' ).replace( />/g, '&gt;' ) + '</textarea>';
826
827 return '<cke:encoded>' + encodeURIComponent( match ) + '</cke:encoded>';
828 } );
829 }
830
831 function unprotectElements( html ) {
832 return html.replace( encodedElementsRegex, function( match, encoded ) {
833 return decodeURIComponent( encoded );
834 } );
835 }
836
837 function protectElementsNames( html ) {
838 return html.replace( protectElementNamesRegex, '$1cke:$2' );
839 }
840
841 function unprotectElementNames( html ) {
842 return html.replace( unprotectElementNamesRegex, '$1$2' );
843 }
844
845 function protectSelfClosingElements( html ) {
846 return html.replace( protectSelfClosingRegex, '<cke:$1$2></cke:$1>' );
847 }
848
849 function protectPreFormatted( html ) {
850 return html.replace( /(<pre\b[^>]*>)(\r\n|\n)/g, '$1$2$2' );
851 }
852
853 function protectRealComments( html ) {
854 return html.replace( /<!--(?!{cke_protected})[\s\S]+?-->/g, function( match ) {
855 return '<!--' + protectedSourceMarker +
856 '{C}' +
857 encodeURIComponent( match ).replace( /--/g, '%2D%2D' ) +
858 '-->';
859 } );
860 }
861
862 // Replace all "on\w{3,}" strings which are not:
863 // * opening tags - e.g. `<onfoo`,
864 // * closing tags - e.g. </onfoo> (tested in "false positive 1"),
865 // * part of other attribute - e.g. `data-onfoo` or `fonfoo`.
866 function protectInsecureAttributes( html ) {
867 return html.replace( /([^a-z0-9<\-])(on\w{3,})(?!>)/gi, '$1data-cke-' + CKEDITOR.rnd + '-$2' );
868 }
869
870 function unprotectRealComments( html ) {
871 return html.replace( /<!--\{cke_protected\}\{C\}([\s\S]+?)-->/g, function( match, data ) {
872 return decodeURIComponent( data );
873 } );
874 }
875
876 function unprotectSource( html, editor ) {
877 var store = editor._.dataStore;
878
879 return html.replace( /<!--\{cke_protected\}([\s\S]+?)-->/g, function( match, data ) {
880 return decodeURIComponent( data );
881 } ).replace( /\{cke_protected_(\d+)\}/g, function( match, id ) {
882 return store && store[ id ] || '';
883 } );
884 }
885
886 function protectSource( data, editor ) {
887 var protectedHtml = [],
888 protectRegexes = editor.config.protectedSource,
889 store = editor._.dataStore || ( editor._.dataStore = { id: 1 } ),
890 tempRegex = /<\!--\{cke_temp(comment)?\}(\d*?)-->/g;
891
892 var regexes = [
893 // Script tags will also be forced to be protected, otherwise
894 // IE will execute them.
895 ( /<script[\s\S]*?(<\/script>|$)/gi ),
896
897 // <noscript> tags (get lost in IE and messed up in FF).
898 /<noscript[\s\S]*?<\/noscript>/gi,
899
900 // Avoid meta tags being stripped (http://dev.ckeditor.com/ticket/8117).
901 /<meta[\s\S]*?\/?>/gi
902 ].concat( protectRegexes );
903
904 // First of any other protection, we must protect all comments
905 // to avoid loosing them (of course, IE related).
906 // Note that we use a different tag for comments, as we need to
907 // transform them when applying filters.
908 data = data.replace( ( /<!--[\s\S]*?-->/g ), function( match ) {
909 return '<!--{cke_tempcomment}' + ( protectedHtml.push( match ) - 1 ) + '-->';
910 } );
911
912 for ( var i = 0; i < regexes.length; i++ ) {
913 data = data.replace( regexes[ i ], function( match ) {
914 match = match.replace( tempRegex, // There could be protected source inside another one. (http://dev.ckeditor.com/ticket/3869).
915 function( $, isComment, id ) {
916 return protectedHtml[ id ];
917 } );
918
919 // Avoid protecting over protected, e.g. /\{.*?\}/
920 return ( /cke_temp(comment)?/ ).test( match ) ? match : '<!--{cke_temp}' + ( protectedHtml.push( match ) - 1 ) + '-->';
921 } );
922 }
923 data = data.replace( tempRegex, function( $, isComment, id ) {
924 return '<!--' + protectedSourceMarker +
925 ( isComment ? '{C}' : '' ) +
926 encodeURIComponent( protectedHtml[ id ] ).replace( /--/g, '%2D%2D' ) +
927 '-->';
928 } );
929
930 // Different protection pattern is used for those that
931 // live in attributes to avoid from being HTML encoded.
932 // Why so serious? See http://dev.ckeditor.com/ticket/9205, http://dev.ckeditor.com/ticket/8216, http://dev.ckeditor.com/ticket/7805, http://dev.ckeditor.com/ticket/11754, http://dev.ckeditor.com/ticket/11846.
933 data = data.replace( /<\w+(?:\s+(?:(?:[^\s=>]+\s*=\s*(?:[^'"\s>]+|'[^']*'|"[^"]*"))|[^\s=\/>]+))+\s*\/?>/g, function( match ) {
934 return match.replace( /<!--\{cke_protected\}([^>]*)-->/g, function( match, data ) {
935 store[ store.id ] = decodeURIComponent( data );
936 return '{cke_protected_' + ( store.id++ ) + '}';
937 } );
938 } );
939
940 // This RegExp searches for innerText in all the title/iframe/textarea elements.
941 // This is because browser doesn't allow HTML in these elements, that's why we can't
942 // nest comments in there. (http://dev.ckeditor.com/ticket/11223)
943 data = data.replace( /<(title|iframe|textarea)([^>]*)>([\s\S]*?)<\/\1>/g, function( match, tagName, tagAttributes, innerText ) {
944 return '<' + tagName + tagAttributes + '>' + unprotectSource( unprotectRealComments( innerText ), editor ) + '</' + tagName + '>';
945 } );
946
947 return data;
948 }
949
950 // Creates a block if the root element is empty.
951 function fixEmptyRoot( root, fixBodyTag ) {
952 if ( !root.children.length && CKEDITOR.dtd[ root.name ][ fixBodyTag ] ) {
953 var fixBodyElement = new CKEDITOR.htmlParser.element( fixBodyTag );
954 root.add( fixBodyElement );
955 }
956 }
957 } )();
958
959 /**
960 * Whether a filler text (non-breaking space entity &mdash; `&nbsp;`) will be
961 * inserted into empty block elements in HTML output.
962 * This is used to render block elements properly with `line-height`.
963 * When a function is specified instead, it will be passed a {@link CKEDITOR.htmlParser.element}
964 * to decide whether adding the filler text by expecting a Boolean return value.
965 *
966 * config.fillEmptyBlocks = false; // Prevent filler nodes in all empty blocks.
967 *
968 * // Prevent filler node only in float cleaners.
969 * config.fillEmptyBlocks = function( element ) {
970 * if ( element.attributes[ 'class' ].indexOf( 'clear-both' ) != -1 )
971 * return false;
972 * };
973 *
974 * @since 3.5
975 * @cfg {Boolean/Function} [fillEmptyBlocks=true]
976 * @member CKEDITOR.config
977 */
978
979 /**
980 * This event is fired by the {@link CKEDITOR.htmlDataProcessor} when input HTML
981 * is to be purified by the {@link CKEDITOR.htmlDataProcessor#toHtml} method.
982 *
983 * By adding listeners with different priorities it is possible
984 * to process input HTML on different stages:
985 *
986 * * 1-4: Data is available in the original string format.
987 * * 5: Data is initially filtered with regexp patterns and parsed to
988 * {@link CKEDITOR.htmlParser.fragment} {@link CKEDITOR.htmlParser.element}.
989 * * 5-9: Data is available in the parsed format, but {@link CKEDITOR.htmlDataProcessor#dataFilter}
990 * is not applied yet.
991 * * 6: Data is filtered with the {@link CKEDITOR.filter content filter}.
992 * * 10: Data is processed with {@link CKEDITOR.htmlDataProcessor#dataFilter}.
993 * * 10-14: Data is available in the parsed format and {@link CKEDITOR.htmlDataProcessor#dataFilter}
994 * has already been applied.
995 * * 15: Data is written back to an HTML string.
996 * * 15-*: Data is available in an HTML string.
997 *
998 * For example to be able to process parsed, but not yet filtered data add listener this way:
999 *
1000 * editor.on( 'toHtml', function( evt) {
1001 * evt.data.dataValue; // -> CKEDITOR.htmlParser.fragment instance
1002 * }, null, null, 7 );
1003 *
1004 * @since 4.1
1005 * @event toHtml
1006 * @member CKEDITOR.editor
1007 * @param {CKEDITOR.editor} editor This editor instance.
1008 * @param data
1009 * @param {String/CKEDITOR.htmlParser.fragment/CKEDITOR.htmlParser.element} data.dataValue Input data to be purified.
1010 * @param {String} data.context See {@link CKEDITOR.htmlDataProcessor#toHtml} The `context` argument.
1011 * @param {Boolean} data.fixForBody See {@link CKEDITOR.htmlDataProcessor#toHtml} The `fixForBody` argument.
1012 * @param {Boolean} data.dontFilter See {@link CKEDITOR.htmlDataProcessor#toHtml} The `dontFilter` argument.
1013 * @param {Boolean} data.filter See {@link CKEDITOR.htmlDataProcessor#toHtml} The `filter` argument.
1014 * @param {Boolean} data.enterMode See {@link CKEDITOR.htmlDataProcessor#toHtml} The `enterMode` argument.
1015 * @param {Boolean} [data.protectedWhitespaces] See {@link CKEDITOR.htmlDataProcessor#toHtml} The `protectedWhitespaces` argument.
1016 */
1017
1018 /**
1019 * This event is fired when {@link CKEDITOR.htmlDataProcessor} is converting
1020 * internal HTML to output data HTML.
1021 *
1022 * By adding listeners with different priorities it is possible
1023 * to process input HTML on different stages:
1024 *
1025 * * 1-4: Data is available in the original string format.
1026 * * 5: Data is initially filtered with regexp patterns and parsed to
1027 * {@link CKEDITOR.htmlParser.fragment} {@link CKEDITOR.htmlParser.element}.
1028 * * 5-9: Data is available in the parsed format, but {@link CKEDITOR.htmlDataProcessor#htmlFilter}
1029 * is not applied yet.
1030 * * 10: Data is filtered with {@link CKEDITOR.htmlDataProcessor#htmlFilter}.
1031 * * 11: Data is filtered with the {CKEDITOR.filter content filter} (on output the content filter makes
1032 * only transformations, without filtering).
1033 * * 10-14: Data is available in the parsed format and {@link CKEDITOR.htmlDataProcessor#htmlFilter}
1034 * has already been applied.
1035 * * 15: Data is written back to an HTML string.
1036 * * 15-*: Data is available in an HTML string.
1037 *
1038 * For example to be able to process parsed and already processed data add listener this way:
1039 *
1040 * editor.on( 'toDataFormat', function( evt) {
1041 * evt.data.dataValue; // -> CKEDITOR.htmlParser.fragment instance
1042 * }, null, null, 12 );
1043 *
1044 * @since 4.1
1045 * @event toDataFormat
1046 * @member CKEDITOR.editor
1047 * @param {CKEDITOR.editor} editor This editor instance.
1048 * @param data
1049 * @param {String/CKEDITOR.htmlParser.fragment/CKEDITOR.htmlParser.element} data.dataValue Output data to be prepared.
1050 * @param {String} data.context See {@link CKEDITOR.htmlDataProcessor#toDataFormat} The `context` argument.
1051 * @param {Boolean} data.filter See {@link CKEDITOR.htmlDataProcessor#toDataFormat} The `filter` argument.
1052 * @param {Boolean} data.enterMode See {@link CKEDITOR.htmlDataProcessor#toDataFormat} The `enterMode` argument.
1053 */