aboutsummaryrefslogtreecommitdiff
path: root/sources/core/htmlparser
diff options
context:
space:
mode:
Diffstat (limited to 'sources/core/htmlparser')
-rw-r--r--sources/core/htmlparser/basicwriter.js152
-rw-r--r--sources/core/htmlparser/cdata.js48
-rw-r--r--sources/core/htmlparser/comment.js80
-rw-r--r--sources/core/htmlparser/element.js568
-rw-r--r--sources/core/htmlparser/filter.js407
-rw-r--r--sources/core/htmlparser/fragment.js646
-rw-r--r--sources/core/htmlparser/node.js156
-rw-r--r--sources/core/htmlparser/text.js70
8 files changed, 2127 insertions, 0 deletions
diff --git a/sources/core/htmlparser/basicwriter.js b/sources/core/htmlparser/basicwriter.js
new file mode 100644
index 0000000..529fbf1
--- /dev/null
+++ b/sources/core/htmlparser/basicwriter.js
@@ -0,0 +1,152 @@
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/**
7 * TODO
8 *
9 * @class
10 * @todo
11 */
12CKEDITOR.htmlParser.basicWriter = CKEDITOR.tools.createClass( {
13 /**
14 * Creates a basicWriter class instance.
15 *
16 * @constructor
17 */
18 $: function() {
19 this._ = {
20 output: []
21 };
22 },
23
24 proto: {
25 /**
26 * Writes the tag opening part for a opener tag.
27 *
28 * // Writes '<p'.
29 * writer.openTag( 'p', { class : 'MyClass', id : 'MyId' } );
30 *
31 * @param {String} tagName The element name for this tag.
32 * @param {Object} attributes The attributes defined for this tag. The
33 * attributes could be used to inspect the tag.
34 */
35 openTag: function( tagName ) {
36 this._.output.push( '<', tagName );
37 },
38
39 /**
40 * Writes the tag closing part for a opener tag.
41 *
42 * // Writes '>'.
43 * writer.openTagClose( 'p', false );
44 *
45 * // Writes ' />'.
46 * writer.openTagClose( 'br', true );
47 *
48 * @param {String} tagName The element name for this tag.
49 * @param {Boolean} isSelfClose Indicates that this is a self-closing tag,
50 * like `<br>` or `<img>`.
51 */
52 openTagClose: function( tagName, isSelfClose ) {
53 if ( isSelfClose )
54 this._.output.push( ' />' );
55 else
56 this._.output.push( '>' );
57 },
58
59 /**
60 * Writes an attribute. This function should be called after opening the
61 * tag with {@link #openTagClose}.
62 *
63 * // Writes ' class="MyClass"'.
64 * writer.attribute( 'class', 'MyClass' );
65 *
66 * @param {String} attName The attribute name.
67 * @param {String} attValue The attribute value.
68 */
69 attribute: function( attName, attValue ) {
70 // Browsers don't always escape special character in attribute values. (#4683, #4719).
71 if ( typeof attValue == 'string' )
72 attValue = CKEDITOR.tools.htmlEncodeAttr( attValue );
73
74 this._.output.push( ' ', attName, '="', attValue, '"' );
75 },
76
77 /**
78 * Writes a closer tag.
79 *
80 * // Writes '</p>'.
81 * writer.closeTag( 'p' );
82 *
83 * @param {String} tagName The element name for this tag.
84 */
85 closeTag: function( tagName ) {
86 this._.output.push( '</', tagName, '>' );
87 },
88
89 /**
90 * Writes text.
91 *
92 * // Writes 'Hello Word'.
93 * writer.text( 'Hello Word' );
94 *
95 * @param {String} text The text value.
96 */
97 text: function( text ) {
98 this._.output.push( text );
99 },
100
101 /**
102 * Writes a comment.
103 *
104 * // Writes '<!-- My comment -->'.
105 * writer.comment( ' My comment ' );
106 *
107 * @param {String} comment The comment text.
108 */
109 comment: function( comment ) {
110 this._.output.push( '<!--', comment, '-->' );
111 },
112
113 /**
114 * Writes any kind of data to the ouput.
115 *
116 * writer.write( 'This is an <b>example</b>.' );
117 *
118 * @param {String} data
119 */
120 write: function( data ) {
121 this._.output.push( data );
122 },
123
124 /**
125 * Empties the current output buffer.
126 *
127 * writer.reset();
128 */
129 reset: function() {
130 this._.output = [];
131 this._.indent = false;
132 },
133
134 /**
135 * Empties the current output buffer.
136 *
137 * var html = writer.getHtml();
138 *
139 * @param {Boolean} reset Indicates that the {@link #reset} method is to
140 * be automatically called after retrieving the HTML.
141 * @returns {String} The HTML written to the writer so far.
142 */
143 getHtml: function( reset ) {
144 var html = this._.output.join( '' );
145
146 if ( reset )
147 this.reset();
148
149 return html;
150 }
151 }
152} );
diff --git a/sources/core/htmlparser/cdata.js b/sources/core/htmlparser/cdata.js
new file mode 100644
index 0000000..be8c5cf
--- /dev/null
+++ b/sources/core/htmlparser/cdata.js
@@ -0,0 +1,48 @@
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 'use strict';
7
8( function() {
9
10 /**
11 * A lightweight representation of HTML CDATA.
12 *
13 * @class
14 * @extends CKEDITOR.htmlParser.node
15 * @constructor Creates a cdata class instance.
16 * @param {String} value The CDATA section value.
17 */
18 CKEDITOR.htmlParser.cdata = function( value ) {
19 /**
20 * The CDATA value.
21 *
22 * @property {String}
23 */
24 this.value = value;
25 };
26
27 CKEDITOR.htmlParser.cdata.prototype = CKEDITOR.tools.extend( new CKEDITOR.htmlParser.node(), {
28 /**
29 * CDATA has the same type as {@link CKEDITOR.htmlParser.text} This is
30 * a constant value set to {@link CKEDITOR#NODE_TEXT}.
31 *
32 * @readonly
33 * @property {Number} [=CKEDITOR.NODE_TEXT]
34 */
35 type: CKEDITOR.NODE_TEXT,
36
37 filter: function() {},
38
39 /**
40 * Writes the CDATA with no special manipulations.
41 *
42 * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML.
43 */
44 writeHtml: function( writer ) {
45 writer.write( this.value );
46 }
47 } );
48} )();
diff --git a/sources/core/htmlparser/comment.js b/sources/core/htmlparser/comment.js
new file mode 100644
index 0000000..14a38f3
--- /dev/null
+++ b/sources/core/htmlparser/comment.js
@@ -0,0 +1,80 @@
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 'use strict';
7
8/**
9 * A lightweight representation of an HTML comment.
10 *
11 * @class
12 * @extends CKEDITOR.htmlParser.node
13 * @constructor Creates a comment class instance.
14 * @param {String} value The comment text value.
15 */
16CKEDITOR.htmlParser.comment = function( value ) {
17 /**
18 * The comment text.
19 *
20 * @property {String}
21 */
22 this.value = value;
23
24 /** @private */
25 this._ = {
26 isBlockLike: false
27 };
28};
29
30CKEDITOR.htmlParser.comment.prototype = CKEDITOR.tools.extend( new CKEDITOR.htmlParser.node(), {
31 /**
32 * The node type. This is a constant value set to {@link CKEDITOR#NODE_COMMENT}.
33 *
34 * @readonly
35 * @property {Number} [=CKEDITOR.NODE_COMMENT]
36 */
37 type: CKEDITOR.NODE_COMMENT,
38
39 /**
40 * Filter this comment with given filter.
41 *
42 * @since 4.1
43 * @param {CKEDITOR.htmlParser.filter} filter
44 * @returns {Boolean} Method returns `false` when this comment has
45 * been removed or replaced with other node. This is an information for
46 * {@link CKEDITOR.htmlParser.element#filterChildren} that it has
47 * to repeat filter on current position in parent's children array.
48 */
49 filter: function( filter, context ) {
50 var comment = this.value;
51
52 if ( !( comment = filter.onComment( context, comment, this ) ) ) {
53 this.remove();
54 return false;
55 }
56
57 if ( typeof comment != 'string' ) {
58 this.replaceWith( comment );
59 return false;
60 }
61
62 this.value = comment;
63
64 return true;
65 },
66
67 /**
68 * Writes the HTML representation of this comment to a CKEDITOR.htmlWriter.
69 *
70 * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML.
71 * @param {CKEDITOR.htmlParser.filter} [filter] The filter to be applied to this node.
72 * **Note:** it's unsafe to filter offline (not appended) node.
73 */
74 writeHtml: function( writer, filter ) {
75 if ( filter )
76 this.filter( filter );
77
78 writer.comment( this.value );
79 }
80} );
diff --git a/sources/core/htmlparser/element.js b/sources/core/htmlparser/element.js
new file mode 100644
index 0000000..0ed750e
--- /dev/null
+++ b/sources/core/htmlparser/element.js
@@ -0,0 +1,568 @@
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'use strict';
7
8/**
9 * A lightweight representation of an HTML element.
10 *
11 * @class
12 * @extends CKEDITOR.htmlParser.node
13 * @constructor Creates an element class instance.
14 * @param {String} name The element name.
15 * @param {Object} attributes An object storing all attributes defined for
16 * this element.
17 */
18CKEDITOR.htmlParser.element = function( name, attributes ) {
19 /**
20 * The element name.
21 *
22 * @property {String}
23 */
24 this.name = name;
25
26 /**
27 * Stores the attributes defined for this element.
28 *
29 * @property {Object}
30 */
31 this.attributes = attributes || {};
32
33 /**
34 * The nodes that are direct children of this element.
35 */
36 this.children = [];
37
38 // Reveal the real semantic of our internal custom tag name (#6639),
39 // when resolving whether it's block like.
40 var realName = name || '',
41 prefixed = realName.match( /^cke:(.*)/ );
42 prefixed && ( realName = prefixed[ 1 ] );
43
44 var isBlockLike = !!( CKEDITOR.dtd.$nonBodyContent[ realName ] || CKEDITOR.dtd.$block[ realName ] ||
45 CKEDITOR.dtd.$listItem[ realName ] || CKEDITOR.dtd.$tableContent[ realName ] ||
46 CKEDITOR.dtd.$nonEditable[ realName ] || realName == 'br' );
47
48 this.isEmpty = !!CKEDITOR.dtd.$empty[ name ];
49 this.isUnknown = !CKEDITOR.dtd[ name ];
50
51 /** @private */
52 this._ = {
53 isBlockLike: isBlockLike,
54 hasInlineStarted: this.isEmpty || !isBlockLike
55 };
56};
57
58/**
59 * Object presentation of the CSS style declaration text.
60 *
61 * @class
62 * @constructor Creates a `cssStyle` class instance.
63 * @param {CKEDITOR.htmlParser.element/String} elementOrStyleText
64 * An HTML parser element or the inline style text.
65 */
66CKEDITOR.htmlParser.cssStyle = function() {
67 var styleText,
68 arg = arguments[ 0 ],
69 rules = {};
70
71 styleText = arg instanceof CKEDITOR.htmlParser.element ? arg.attributes.style : arg;
72
73 // html-encoded quote might be introduced by 'font-family'
74 // from MS-Word which confused the following regexp. e.g.
75 //'font-family: &quot;Lucida, Console&quot;'
76 // TODO reuse CSS methods from tools.
77 ( styleText || '' ).replace( /&quot;/g, '"' ).replace( /\s*([^ :;]+)\s*:\s*([^;]+)\s*(?=;|$)/g, function( match, name, value ) {
78 name == 'font-family' && ( value = value.replace( /["']/g, '' ) );
79 rules[ name.toLowerCase() ] = value;
80 } );
81
82 return {
83
84 rules: rules,
85
86 /**
87 * Applies the styles to the specified element or object.
88 *
89 * @param {CKEDITOR.htmlParser.element/CKEDITOR.dom.element/Object} obj
90 */
91 populate: function( obj ) {
92 var style = this.toString();
93 if ( style )
94 obj instanceof CKEDITOR.dom.element ? obj.setAttribute( 'style', style ) : obj instanceof CKEDITOR.htmlParser.element ? obj.attributes.style = style : obj.style = style;
95
96 },
97
98 /**
99 * Serializes CSS style declaration to a string.
100 *
101 * @returns {String}
102 */
103 toString: function() {
104 var output = [];
105 for ( var i in rules )
106 rules[ i ] && output.push( i, ':', rules[ i ], ';' );
107 return output.join( '' );
108 }
109 };
110};
111
112/** @class CKEDITOR.htmlParser.element */
113( function() {
114 // Used to sort attribute entries in an array, where the first element of
115 // each object is the attribute name.
116 var sortAttribs = function( a, b ) {
117 a = a[ 0 ];
118 b = b[ 0 ];
119 return a < b ? -1 : a > b ? 1 : 0;
120 },
121 fragProto = CKEDITOR.htmlParser.fragment.prototype;
122
123 CKEDITOR.htmlParser.element.prototype = CKEDITOR.tools.extend( new CKEDITOR.htmlParser.node(), {
124 /**
125 * The node type. This is a constant value set to {@link CKEDITOR#NODE_ELEMENT}.
126 *
127 * @readonly
128 * @property {Number} [=CKEDITOR.NODE_ELEMENT]
129 */
130 type: CKEDITOR.NODE_ELEMENT,
131
132 /**
133 * Adds a node to the element children list.
134 *
135 * @method
136 * @param {CKEDITOR.htmlParser.node} node The node to be added.
137 * @param {Number} [index] From where the insertion happens.
138 */
139 add: fragProto.add,
140
141 /**
142 * Clones this element.
143 *
144 * @returns {CKEDITOR.htmlParser.element} The element clone.
145 */
146 clone: function() {
147 return new CKEDITOR.htmlParser.element( this.name, this.attributes );
148 },
149
150 /**
151 * Filters this element and its children with the given filter.
152 *
153 * @since 4.1
154 * @param {CKEDITOR.htmlParser.filter} filter
155 * @returns {Boolean} The method returns `false` when this element has
156 * been removed or replaced with another. This information means that
157 * {@link #filterChildren} has to repeat the filter on the current
158 * position in parent's children array.
159 */
160 filter: function( filter, context ) {
161 var element = this,
162 originalName, name;
163
164 context = element.getFilterContext( context );
165
166 // Do not process elements with data-cke-processor attribute set to off.
167 if ( context.off )
168 return true;
169
170 // Filtering if it's the root node.
171 if ( !element.parent )
172 filter.onRoot( context, element );
173
174 while ( true ) {
175 originalName = element.name;
176
177 if ( !( name = filter.onElementName( context, originalName ) ) ) {
178 this.remove();
179 return false;
180 }
181
182 element.name = name;
183
184 if ( !( element = filter.onElement( context, element ) ) ) {
185 this.remove();
186 return false;
187 }
188
189 // New element has been returned - replace current one
190 // and process it (stop processing this and return false, what
191 // means that element has been removed).
192 if ( element !== this ) {
193 this.replaceWith( element );
194 return false;
195 }
196
197 // If name has been changed - continue loop, so in next iteration
198 // filters for new name will be applied to this element.
199 // If name hasn't been changed - stop.
200 if ( element.name == originalName )
201 break;
202
203 // If element has been replaced with something of a
204 // different type, then make the replacement filter itself.
205 if ( element.type != CKEDITOR.NODE_ELEMENT ) {
206 this.replaceWith( element );
207 return false;
208 }
209
210 // This indicate that the element has been dropped by
211 // filter but not the children.
212 if ( !element.name ) {
213 this.replaceWithChildren();
214 return false;
215 }
216 }
217
218 var attributes = element.attributes,
219 a, value, newAttrName;
220
221 for ( a in attributes ) {
222 newAttrName = a;
223 value = attributes[ a ];
224
225 // Loop until name isn't modified.
226 // A little bit senseless, but IE would do that anyway
227 // because it iterates with for-in loop even over properties
228 // created during its run.
229 while ( true ) {
230 if ( !( newAttrName = filter.onAttributeName( context, a ) ) ) {
231 delete attributes[ a ];
232 break;
233 } else if ( newAttrName != a ) {
234 delete attributes[ a ];
235 a = newAttrName;
236 continue;
237 } else {
238 break;
239 }
240 }
241
242 if ( newAttrName ) {
243 if ( ( value = filter.onAttribute( context, element, newAttrName, value ) ) === false )
244 delete attributes[ newAttrName ];
245 else
246 attributes[ newAttrName ] = value;
247 }
248 }
249
250 if ( !element.isEmpty )
251 this.filterChildren( filter, false, context );
252
253 return true;
254 },
255
256 /**
257 * Filters this element's children with the given filter.
258 *
259 * Element's children may only be filtered once by one
260 * instance of the filter.
261 *
262 * @method filterChildren
263 * @param {CKEDITOR.htmlParser.filter} filter
264 */
265 filterChildren: fragProto.filterChildren,
266
267 /**
268 * Writes the element HTML to the CKEDITOR.htmlWriter.
269 *
270 * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which HTML will be written.
271 * @param {CKEDITOR.htmlParser.filter} [filter] The filter to be applied to this node.
272 * **Note:** It is unsafe to filter an offline (not appended) node.
273 */
274 writeHtml: function( writer, filter ) {
275 if ( filter )
276 this.filter( filter );
277
278 var name = this.name,
279 attribsArray = [],
280 attributes = this.attributes,
281 attrName,
282 attr, i, l;
283
284 // Open element tag.
285 writer.openTag( name, attributes );
286
287 // Copy all attributes to an array.
288 for ( attrName in attributes )
289 attribsArray.push( [ attrName, attributes[ attrName ] ] );
290
291 // Sort the attributes by name.
292 if ( writer.sortAttributes )
293 attribsArray.sort( sortAttribs );
294
295 // Send the attributes.
296 for ( i = 0, l = attribsArray.length; i < l; i++ ) {
297 attr = attribsArray[ i ];
298 writer.attribute( attr[ 0 ], attr[ 1 ] );
299 }
300
301 // Close the tag.
302 writer.openTagClose( name, this.isEmpty );
303
304 this.writeChildrenHtml( writer );
305
306 // Close the element.
307 if ( !this.isEmpty )
308 writer.closeTag( name );
309 },
310
311 /**
312 * Sends children of this element to the writer.
313 *
314 * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which HTML will be written.
315 * @param {CKEDITOR.htmlParser.filter} [filter]
316 */
317 writeChildrenHtml: fragProto.writeChildrenHtml,
318
319 /**
320 * Replaces this element with its children.
321 *
322 * @since 4.1
323 */
324 replaceWithChildren: function() {
325 var children = this.children;
326
327 for ( var i = children.length; i; )
328 children[ --i ].insertAfter( this );
329
330 this.remove();
331 },
332
333 /**
334 * Executes a callback on each node (of the given type) in this element.
335 *
336 * // Create a <p> element with foo<b>bar</b>bom as its content.
337 * var elP = CKEDITOR.htmlParser.fragment.fromHtml( 'foo<b>bar</b>bom', 'p' );
338 * elP.forEach( function( node ) {
339 * console.log( node );
340 * } );
341 * // Will log:
342 * // 1. document fragment,
343 * // 2. <p> element,
344 * // 3. "foo" text node,
345 * // 4. <b> element,
346 * // 5. "bar" text node,
347 * // 6. "bom" text node.
348 *
349 * @since 4.1
350 * @param {Function} callback Function to be executed on every node.
351 * **Since 4.3**: If `callback` returned `false`, the descendants of the current node will be ignored.
352 * @param {CKEDITOR.htmlParser.node} callback.node Node passed as an argument.
353 * @param {Number} [type] Whether the specified `callback` will be executed only on nodes of this type.
354 * @param {Boolean} [skipRoot] Do not execute `callback` on this element.
355 */
356 forEach: fragProto.forEach,
357
358 /**
359 * Gets this element's first child. If `condition` is given, this method returns
360 * the first child which satisfies that condition.
361 *
362 * @since 4.3
363 * @param {String/Object/Function} condition Name of a child, a hash of names, or a validator function.
364 * @returns {CKEDITOR.htmlParser.node}
365 */
366 getFirst: function( condition ) {
367 if ( !condition )
368 return this.children.length ? this.children[ 0 ] : null;
369
370 if ( typeof condition != 'function' )
371 condition = nameCondition( condition );
372
373 for ( var i = 0, l = this.children.length; i < l; ++i ) {
374 if ( condition( this.children[ i ] ) )
375 return this.children[ i ];
376 }
377 return null;
378 },
379
380 /**
381 * Gets this element's inner HTML.
382 *
383 * @since 4.3
384 * @returns {String}
385 */
386 getHtml: function() {
387 var writer = new CKEDITOR.htmlParser.basicWriter();
388 this.writeChildrenHtml( writer );
389 return writer.getHtml();
390 },
391
392 /**
393 * Sets this element's inner HTML.
394 *
395 * @since 4.3
396 * @param {String} html
397 */
398 setHtml: function( html ) {
399 var children = this.children = CKEDITOR.htmlParser.fragment.fromHtml( html ).children;
400
401 for ( var i = 0, l = children.length; i < l; ++i )
402 children[ i ].parent = this;
403 },
404
405 /**
406 * Gets this element's outer HTML.
407 *
408 * @since 4.3
409 * @returns {String}
410 */
411 getOuterHtml: function() {
412 var writer = new CKEDITOR.htmlParser.basicWriter();
413 this.writeHtml( writer );
414 return writer.getHtml();
415 },
416
417 /**
418 * Splits this element at the given index.
419 *
420 * @since 4.3
421 * @param {Number} index Index at which the element will be split &mdash; `0` means the beginning,
422 * `1` after the first child node, etc.
423 * @returns {CKEDITOR.htmlParser.element} The new element following this one.
424 */
425 split: function( index ) {
426 var cloneChildren = this.children.splice( index, this.children.length - index ),
427 clone = this.clone();
428
429 for ( var i = 0; i < cloneChildren.length; ++i )
430 cloneChildren[ i ].parent = clone;
431
432 clone.children = cloneChildren;
433
434 if ( cloneChildren[ 0 ] )
435 cloneChildren[ 0 ].previous = null;
436
437 if ( index > 0 )
438 this.children[ index - 1 ].next = null;
439
440 this.parent.add( clone, this.getIndex() + 1 );
441
442 return clone;
443 },
444
445 /**
446 * Searches through the current node children to find nodes matching the `criteria`.
447 *
448 * @param {String/Function} criteria Tag name or evaluator function.
449 * @param {Boolean} [recursive=false]
450 * @returns {CKEDITOR.htmlParser.node[]}
451 */
452 find: function( criteria, recursive ) {
453 if ( recursive === undefined ) {
454 recursive = false;
455 }
456
457 var ret = [],
458 i;
459
460 for ( i = 0; i < this.children.length; i++ ) {
461 var curChild = this.children[ i ];
462
463 if ( typeof criteria == 'function' && criteria( curChild ) ) {
464 ret.push( curChild );
465 } else if ( typeof criteria == 'string' && curChild.name === criteria ) {
466 ret.push( curChild );
467 }
468
469 if ( recursive && curChild.find ) {
470 ret = ret.concat( curChild.find( criteria, recursive ) );
471 }
472 }
473
474 return ret;
475 },
476
477 /**
478 * Adds a class name to the list of classes.
479 *
480 * @since 4.4
481 * @param {String} className The class name to be added.
482 */
483 addClass: function( className ) {
484 if ( this.hasClass( className ) )
485 return;
486
487 var c = this.attributes[ 'class' ] || '';
488
489 this.attributes[ 'class' ] = c + ( c ? ' ' : '' ) + className;
490 },
491
492 /**
493 * Removes a class name from the list of classes.
494 *
495 * @since 4.3
496 * @param {String} className The class name to be removed.
497 */
498 removeClass: function( className ) {
499 var classes = this.attributes[ 'class' ];
500
501 if ( !classes )
502 return;
503
504 // We can safely assume that className won't break regexp.
505 // http://stackoverflow.com/questions/448981/what-characters-are-valid-in-css-class-names
506 classes = CKEDITOR.tools.trim( classes.replace( new RegExp( '(?:\\s+|^)' + className + '(?:\\s+|$)' ), ' ' ) );
507
508 if ( classes )
509 this.attributes[ 'class' ] = classes;
510 else
511 delete this.attributes[ 'class' ];
512 },
513
514 /**
515 * Checkes whether this element has a class name.
516 *
517 * @since 4.3
518 * @param {String} className The class name to be checked.
519 * @returns {Boolean} Whether this element has a `className`.
520 */
521 hasClass: function( className ) {
522 var classes = this.attributes[ 'class' ];
523
524 if ( !classes )
525 return false;
526
527 return ( new RegExp( '(?:^|\\s)' + className + '(?=\\s|$)' ) ).test( classes );
528 },
529
530 getFilterContext: function( ctx ) {
531 var changes = [];
532
533 if ( !ctx ) {
534 ctx = {
535 off: false,
536 nonEditable: false,
537 nestedEditable: false
538 };
539 }
540
541 if ( !ctx.off && this.attributes[ 'data-cke-processor' ] == 'off' )
542 changes.push( 'off', true );
543
544 if ( !ctx.nonEditable && this.attributes.contenteditable == 'false' )
545 changes.push( 'nonEditable', true );
546 // A context to be given nestedEditable must be nonEditable first (by inheritance) (#11372, #11698).
547 // Special case: #11504 - filter starts on <body contenteditable=true>,
548 // so ctx.nonEditable has not been yet set to true.
549 else if ( ctx.nonEditable && !ctx.nestedEditable && this.attributes.contenteditable == 'true' )
550 changes.push( 'nestedEditable', true );
551
552 if ( changes.length ) {
553 ctx = CKEDITOR.tools.copy( ctx );
554 for ( var i = 0; i < changes.length; i += 2 )
555 ctx[ changes[ i ] ] = changes[ i + 1 ];
556 }
557
558 return ctx;
559 }
560 }, true );
561
562 function nameCondition( condition ) {
563 return function( el ) {
564 return el.type == CKEDITOR.NODE_ELEMENT &&
565 ( typeof condition == 'string' ? el.name == condition : el.name in condition );
566 };
567 }
568} )();
diff --git a/sources/core/htmlparser/filter.js b/sources/core/htmlparser/filter.js
new file mode 100644
index 0000000..db6b91c
--- /dev/null
+++ b/sources/core/htmlparser/filter.js
@@ -0,0 +1,407 @@
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'use strict';
7
8( function() {
9 /**
10 * Filter is a configurable tool for transforming and filtering {@link CKEDITOR.htmlParser.node nodes}.
11 * It is mainly used during data processing phase which is done not on real DOM nodes,
12 * but on their simplified form represented by {@link CKEDITOR.htmlParser.node} class and its subclasses.
13 *
14 * var filter = new CKEDITOR.htmlParser.filter( {
15 * text: function( value ) {
16 * return '@' + value + '@';
17 * },
18 * elements: {
19 * p: function( element ) {
20 * element.attributes.foo = '1';
21 * }
22 * }
23 * } );
24 *
25 * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<p>Foo<b>bar!</b></p>' ),
26 * writer = new CKEDITOR.htmlParser.basicWriter();
27 * filter.applyTo( fragment );
28 * fragment.writeHtml( writer );
29 * writer.getHtml(); // '<p foo="1">@Foo@<b>@bar!@</b></p>'
30 *
31 * @class
32 */
33 CKEDITOR.htmlParser.filter = CKEDITOR.tools.createClass( {
34 /**
35 * @constructor Creates a filter class instance.
36 * @param {CKEDITOR.htmlParser.filterRulesDefinition} [rules]
37 */
38 $: function( rules ) {
39 /**
40 * ID of filter instance, which is used to mark elements
41 * to which this filter has been already applied.
42 *
43 * @property {Number} id
44 * @readonly
45 */
46 this.id = CKEDITOR.tools.getNextNumber();
47
48 /**
49 * Rules for element names.
50 *
51 * @property {CKEDITOR.htmlParser.filterRulesGroup}
52 * @readonly
53 */
54 this.elementNameRules = new filterRulesGroup();
55
56 /**
57 * Rules for attribute names.
58 *
59 * @property {CKEDITOR.htmlParser.filterRulesGroup}
60 * @readonly
61 */
62 this.attributeNameRules = new filterRulesGroup();
63
64 /**
65 * Hash of elementName => {@link CKEDITOR.htmlParser.filterRulesGroup rules for elements}.
66 *
67 * @readonly
68 */
69 this.elementsRules = {};
70
71 /**
72 * Hash of attributeName => {@link CKEDITOR.htmlParser.filterRulesGroup rules for attributes}.
73 *
74 * @readonly
75 */
76 this.attributesRules = {};
77
78 /**
79 * Rules for text nodes.
80 *
81 * @property {CKEDITOR.htmlParser.filterRulesGroup}
82 * @readonly
83 */
84 this.textRules = new filterRulesGroup();
85
86 /**
87 * Rules for comment nodes.
88 *
89 * @property {CKEDITOR.htmlParser.filterRulesGroup}
90 * @readonly
91 */
92 this.commentRules = new filterRulesGroup();
93
94 /**
95 * Rules for a root node.
96 *
97 * @property {CKEDITOR.htmlParser.filterRulesGroup}
98 * @readonly
99 */
100 this.rootRules = new filterRulesGroup();
101
102 if ( rules )
103 this.addRules( rules, 10 );
104 },
105
106 proto: {
107 /**
108 * Add rules to this filter.
109 *
110 * @param {CKEDITOR.htmlParser.filterRulesDefinition} rules Object containing filter rules.
111 * @param {Object/Number} [options] Object containing rules' options or a priority
112 * (for a backward compatibility with CKEditor versions up to 4.2.x).
113 * @param {Number} [options.priority=10] The priority of a rule.
114 * @param {Boolean} [options.applyToAll=false] Whether to apply rule to non-editable
115 * elements and their descendants too.
116 */
117 addRules: function( rules, options ) {
118 var priority;
119
120 // Backward compatibility.
121 if ( typeof options == 'number' )
122 priority = options;
123 // New version - try reading from options.
124 else if ( options && ( 'priority' in options ) )
125 priority = options.priority;
126
127 // Defaults.
128 if ( typeof priority != 'number' )
129 priority = 10;
130 if ( typeof options != 'object' )
131 options = {};
132
133 // Add the elementNames.
134 if ( rules.elementNames )
135 this.elementNameRules.addMany( rules.elementNames, priority, options );
136
137 // Add the attributeNames.
138 if ( rules.attributeNames )
139 this.attributeNameRules.addMany( rules.attributeNames, priority, options );
140
141 // Add the elements.
142 if ( rules.elements )
143 addNamedRules( this.elementsRules, rules.elements, priority, options );
144
145 // Add the attributes.
146 if ( rules.attributes )
147 addNamedRules( this.attributesRules, rules.attributes, priority, options );
148
149 // Add the text.
150 if ( rules.text )
151 this.textRules.add( rules.text, priority, options );
152
153 // Add the comment.
154 if ( rules.comment )
155 this.commentRules.add( rules.comment, priority, options );
156
157 // Add root node rules.
158 if ( rules.root )
159 this.rootRules.add( rules.root, priority, options );
160 },
161
162 /**
163 * Apply this filter to given node.
164 *
165 * @param {CKEDITOR.htmlParser.node} node The node to be filtered.
166 */
167 applyTo: function( node ) {
168 node.filter( this );
169 },
170
171 onElementName: function( context, name ) {
172 return this.elementNameRules.execOnName( context, name );
173 },
174
175 onAttributeName: function( context, name ) {
176 return this.attributeNameRules.execOnName( context, name );
177 },
178
179 onText: function( context, text, node ) {
180 return this.textRules.exec( context, text, node );
181 },
182
183 onComment: function( context, commentText, comment ) {
184 return this.commentRules.exec( context, commentText, comment );
185 },
186
187 onRoot: function( context, element ) {
188 return this.rootRules.exec( context, element );
189 },
190
191 onElement: function( context, element ) {
192 // We must apply filters set to the specific element name as
193 // well as those set to the generic ^/$ name. So, add both to an
194 // array and process them in a small loop.
195 var rulesGroups = [ this.elementsRules[ '^' ], this.elementsRules[ element.name ], this.elementsRules.$ ],
196 rulesGroup, ret;
197
198 for ( var i = 0; i < 3; i++ ) {
199 rulesGroup = rulesGroups[ i ];
200 if ( rulesGroup ) {
201 ret = rulesGroup.exec( context, element, this );
202
203 if ( ret === false )
204 return null;
205
206 if ( ret && ret != element )
207 return this.onNode( context, ret );
208
209 // The non-root element has been dismissed by one of the filters.
210 if ( element.parent && !element.name )
211 break;
212 }
213 }
214
215 return element;
216 },
217
218 onNode: function( context, node ) {
219 var type = node.type;
220
221 return type == CKEDITOR.NODE_ELEMENT ? this.onElement( context, node ) :
222 type == CKEDITOR.NODE_TEXT ? new CKEDITOR.htmlParser.text( this.onText( context, node.value ) ) :
223 type == CKEDITOR.NODE_COMMENT ? new CKEDITOR.htmlParser.comment( this.onComment( context, node.value ) ) : null;
224 },
225
226 onAttribute: function( context, element, name, value ) {
227 var rulesGroup = this.attributesRules[ name ];
228
229 if ( rulesGroup )
230 return rulesGroup.exec( context, value, element, this );
231 return value;
232 }
233 }
234 } );
235
236 /**
237 * Class grouping filter rules for one subject (like element or attribute names).
238 *
239 * @class CKEDITOR.htmlParser.filterRulesGroup
240 */
241 function filterRulesGroup() {
242 /**
243 * Array of objects containing rule, priority and options.
244 *
245 * @property {Object[]}
246 * @readonly
247 */
248 this.rules = [];
249 }
250
251 CKEDITOR.htmlParser.filterRulesGroup = filterRulesGroup;
252
253 filterRulesGroup.prototype = {
254 /**
255 * Adds specified rule to this group.
256 *
257 * @param {Function/Array} rule Function for function based rule or [ pattern, replacement ] array for
258 * rule applicable to names.
259 * @param {Number} priority
260 * @param options
261 */
262 add: function( rule, priority, options ) {
263 this.rules.splice( this.findIndex( priority ), 0, {
264 value: rule,
265 priority: priority,
266 options: options
267 } );
268 },
269
270 /**
271 * Adds specified rules to this group.
272 *
273 * @param {Array} rules Array of rules - see {@link #add}.
274 * @param {Number} priority
275 * @param options
276 */
277 addMany: function( rules, priority, options ) {
278 var args = [ this.findIndex( priority ), 0 ];
279
280 for ( var i = 0, len = rules.length; i < len; i++ ) {
281 args.push( {
282 value: rules[ i ],
283 priority: priority,
284 options: options
285 } );
286 }
287
288 this.rules.splice.apply( this.rules, args );
289 },
290
291 /**
292 * Finds an index at which rule with given priority should be inserted.
293 *
294 * @param {Number} priority
295 * @returns {Number} Index.
296 */
297 findIndex: function( priority ) {
298 var rules = this.rules,
299 len = rules.length,
300 i = len - 1;
301
302 // Search from the end, because usually rules will be added with default priority, so
303 // we will be able to stop loop quickly.
304 while ( i >= 0 && priority < rules[ i ].priority )
305 i--;
306
307 return i + 1;
308 },
309
310 /**
311 * Executes this rules group on given value. Applicable only if function based rules were added.
312 *
313 * All arguments passed to this function will be forwarded to rules' functions.
314 *
315 * @param {CKEDITOR.htmlParser.node/CKEDITOR.htmlParser.fragment/String} currentValue The value to be filtered.
316 * @returns {CKEDITOR.htmlParser.node/CKEDITOR.htmlParser.fragment/String} Filtered value.
317 */
318 exec: function( context, currentValue ) {
319 var isNode = currentValue instanceof CKEDITOR.htmlParser.node || currentValue instanceof CKEDITOR.htmlParser.fragment,
320 // Splice '1' to remove context, which we don't want to pass to filter rules.
321 args = Array.prototype.slice.call( arguments, 1 ),
322 rules = this.rules,
323 len = rules.length,
324 orgType, orgName, ret, i, rule;
325
326 for ( i = 0; i < len; i++ ) {
327 // Backup the node info before filtering.
328 if ( isNode ) {
329 orgType = currentValue.type;
330 orgName = currentValue.name;
331 }
332
333 rule = rules[ i ];
334 if ( isRuleApplicable( context, rule ) ) {
335 ret = rule.value.apply( null, args );
336
337 if ( ret === false )
338 return ret;
339
340 // We're filtering node (element/fragment).
341 // No further filtering if it's not anymore fitable for the subsequent filters.
342 if ( isNode && ret && ( ret.name != orgName || ret.type != orgType ) )
343 return ret;
344
345 // Update currentValue and corresponding argument in args array.
346 // Updated values will be used in next for-loop step.
347 if ( ret != null )
348 args[ 0 ] = currentValue = ret;
349
350 // ret == undefined will continue loop as nothing has happened.
351 }
352 }
353
354 return currentValue;
355 },
356
357 /**
358 * Executes this rules group on name. Applicable only if filter rules for names were added.
359 *
360 * @param {String} currentName The name to be filtered.
361 * @returns {String} Filtered name.
362 */
363 execOnName: function( context, currentName ) {
364 var i = 0,
365 rules = this.rules,
366 len = rules.length,
367 rule;
368
369 for ( ; currentName && i < len; i++ ) {
370 rule = rules[ i ];
371 if ( isRuleApplicable( context, rule ) )
372 currentName = currentName.replace( rule.value[ 0 ], rule.value[ 1 ] );
373 }
374
375 return currentName;
376 }
377 };
378
379 function addNamedRules( rulesGroups, newRules, priority, options ) {
380 var ruleName, rulesGroup;
381
382 for ( ruleName in newRules ) {
383 rulesGroup = rulesGroups[ ruleName ];
384
385 if ( !rulesGroup )
386 rulesGroup = rulesGroups[ ruleName ] = new filterRulesGroup();
387
388 rulesGroup.add( newRules[ ruleName ], priority, options );
389 }
390 }
391
392 function isRuleApplicable( context, rule ) {
393 if ( context.nonEditable && !rule.options.applyToAll )
394 return false;
395
396 if ( context.nestedEditable && rule.options.excludeNestedEditable )
397 return false;
398
399 return true;
400 }
401
402} )();
403
404/**
405 * @class CKEDITOR.htmlParser.filterRulesDefinition
406 * @abstract
407 */
diff --git a/sources/core/htmlparser/fragment.js b/sources/core/htmlparser/fragment.js
new file mode 100644
index 0000000..f696a12
--- /dev/null
+++ b/sources/core/htmlparser/fragment.js
@@ -0,0 +1,646 @@
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'use strict';
7
8/**
9 * A lightweight representation of an HTML DOM structure.
10 *
11 * @class
12 * @constructor Creates a fragment class instance.
13 */
14CKEDITOR.htmlParser.fragment = function() {
15 /**
16 * The nodes contained in the root of this fragment.
17 *
18 * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<b>Sample</b> Text' );
19 * alert( fragment.children.length ); // 2
20 */
21 this.children = [];
22
23 /**
24 * Get the fragment parent. Should always be null.
25 *
26 * @property {Object} [=null]
27 */
28 this.parent = null;
29
30 /** @private */
31 this._ = {
32 isBlockLike: true,
33 hasInlineStarted: false
34 };
35};
36
37( function() {
38 // Block-level elements whose internal structure should be respected during
39 // parser fixing.
40 var nonBreakingBlocks = CKEDITOR.tools.extend( { table: 1, ul: 1, ol: 1, dl: 1 }, CKEDITOR.dtd.table, CKEDITOR.dtd.ul, CKEDITOR.dtd.ol, CKEDITOR.dtd.dl );
41
42 var listBlocks = { ol: 1, ul: 1 };
43
44 // Dtd of the fragment element, basically it accept anything except for intermediate structure, e.g. orphan <li>.
45 var rootDtd = CKEDITOR.tools.extend( {}, { html: 1 }, CKEDITOR.dtd.html, CKEDITOR.dtd.body, CKEDITOR.dtd.head, { style: 1, script: 1 } );
46
47 // Which element to create when encountered not allowed content.
48 var structureFixes = {
49 ul: 'li',
50 ol: 'li',
51 dl: 'dd',
52 table: 'tbody',
53 tbody: 'tr',
54 thead: 'tr',
55 tfoot: 'tr',
56 tr: 'td'
57 };
58
59 function isRemoveEmpty( node ) {
60 // Keep marked element event if it is empty.
61 if ( node.attributes[ 'data-cke-survive' ] )
62 return false;
63
64 // Empty link is to be removed when empty but not anchor. (#7894)
65 return node.name == 'a' && node.attributes.href || CKEDITOR.dtd.$removeEmpty[ node.name ];
66 }
67
68 /**
69 * Creates a {@link CKEDITOR.htmlParser.fragment} from an HTML string.
70 *
71 * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<b>Sample</b> Text' );
72 * alert( fragment.children[ 0 ].name ); // 'b'
73 * alert( fragment.children[ 1 ].value ); // ' Text'
74 *
75 * @static
76 * @param {String} fragmentHtml The HTML to be parsed, filling the fragment.
77 * @param {CKEDITOR.htmlParser.element/String} [parent] Optional contextual
78 * element which makes the content been parsed as the content of this element and fix
79 * to match it.
80 * If not provided, then {@link CKEDITOR.htmlParser.fragment} will be used
81 * as the parent and it will be returned.
82 * @param {String/Boolean} [fixingBlock] When `parent` is a block limit element,
83 * and the param is a string value other than `false`, it is to
84 * avoid having block-less content as the direct children of parent by wrapping
85 * the content with a block element of the specified tag, e.g.
86 * when `fixingBlock` specified as `p`, the content `<body><i>foo</i></body>`
87 * will be fixed into `<body><p><i>foo</i></p></body>`.
88 * @returns {CKEDITOR.htmlParser.fragment/CKEDITOR.htmlParser.element} The created fragment or passed `parent`.
89 */
90 CKEDITOR.htmlParser.fragment.fromHtml = function( fragmentHtml, parent, fixingBlock ) {
91 var parser = new CKEDITOR.htmlParser();
92
93 var root = parent instanceof CKEDITOR.htmlParser.element ? parent : typeof parent == 'string' ? new CKEDITOR.htmlParser.element( parent ) : new CKEDITOR.htmlParser.fragment();
94
95 var pendingInline = [],
96 pendingBRs = [],
97 currentNode = root,
98 // Indicate we're inside a <textarea> element, spaces should be touched differently.
99 inTextarea = root.name == 'textarea',
100 // Indicate we're inside a <pre> element, spaces should be touched differently.
101 inPre = root.name == 'pre';
102
103 function checkPending( newTagName ) {
104 var pendingBRsSent;
105
106 if ( pendingInline.length > 0 ) {
107 for ( var i = 0; i < pendingInline.length; i++ ) {
108 var pendingElement = pendingInline[ i ],
109 pendingName = pendingElement.name,
110 pendingDtd = CKEDITOR.dtd[ pendingName ],
111 currentDtd = currentNode.name && CKEDITOR.dtd[ currentNode.name ];
112
113 if ( ( !currentDtd || currentDtd[ pendingName ] ) && ( !newTagName || !pendingDtd || pendingDtd[ newTagName ] || !CKEDITOR.dtd[ newTagName ] ) ) {
114 if ( !pendingBRsSent ) {
115 sendPendingBRs();
116 pendingBRsSent = 1;
117 }
118
119 // Get a clone for the pending element.
120 pendingElement = pendingElement.clone();
121
122 // Add it to the current node and make it the current,
123 // so the new element will be added inside of it.
124 pendingElement.parent = currentNode;
125 currentNode = pendingElement;
126
127 // Remove the pending element (back the index by one
128 // to properly process the next entry).
129 pendingInline.splice( i, 1 );
130 i--;
131 } else {
132 // Some element of the same type cannot be nested, flat them,
133 // e.g. <a href="#">foo<a href="#">bar</a></a>. (#7894)
134 if ( pendingName == currentNode.name )
135 addElement( currentNode, currentNode.parent, 1 ), i--;
136 }
137 }
138 }
139 }
140
141 function sendPendingBRs() {
142 while ( pendingBRs.length )
143 addElement( pendingBRs.shift(), currentNode );
144 }
145
146 // Rtrim empty spaces on block end boundary. (#3585)
147 function removeTailWhitespace( element ) {
148 if ( element._.isBlockLike && element.name != 'pre' && element.name != 'textarea' ) {
149
150 var length = element.children.length,
151 lastChild = element.children[ length - 1 ],
152 text;
153 if ( lastChild && lastChild.type == CKEDITOR.NODE_TEXT ) {
154 if ( !( text = CKEDITOR.tools.rtrim( lastChild.value ) ) )
155 element.children.length = length - 1;
156 else
157 lastChild.value = text;
158 }
159 }
160 }
161
162 // Beside of simply append specified element to target, this function also takes
163 // care of other dirty lifts like forcing block in body, trimming spaces at
164 // the block boundaries etc.
165 //
166 // @param {Element} element The element to be added as the last child of {@link target}.
167 // @param {Element} target The parent element to relieve the new node.
168 // @param {Boolean} [moveCurrent=false] Don't change the "currentNode" global unless
169 // there's a return point node specified on the element, otherwise move current onto {@link target} node.
170 //
171 function addElement( element, target, moveCurrent ) {
172 target = target || currentNode || root;
173
174 // Current element might be mangled by fix body below,
175 // save it for restore later.
176 var savedCurrent = currentNode;
177
178 // Ignore any element that has already been added.
179 if ( element.previous === undefined ) {
180 if ( checkAutoParagraphing( target, element ) ) {
181 // Create a <p> in the fragment.
182 currentNode = target;
183 parser.onTagOpen( fixingBlock, {} );
184
185 // The new target now is the <p>.
186 element.returnPoint = target = currentNode;
187 }
188
189 removeTailWhitespace( element );
190
191 // Avoid adding empty inline.
192 if ( !( isRemoveEmpty( element ) && !element.children.length ) )
193 target.add( element );
194
195 if ( element.name == 'pre' )
196 inPre = false;
197
198 if ( element.name == 'textarea' )
199 inTextarea = false;
200 }
201
202 if ( element.returnPoint ) {
203 currentNode = element.returnPoint;
204 delete element.returnPoint;
205 } else {
206 currentNode = moveCurrent ? target : savedCurrent;
207 }
208 }
209
210 // Auto paragraphing should happen when inline content enters the root element.
211 function checkAutoParagraphing( parent, node ) {
212
213 // Check for parent that can contain block.
214 if ( ( parent == root || parent.name == 'body' ) && fixingBlock &&
215 ( !parent.name || CKEDITOR.dtd[ parent.name ][ fixingBlock ] ) ) {
216 var name, realName;
217
218 if ( node.attributes && ( realName = node.attributes[ 'data-cke-real-element-type' ] ) )
219 name = realName;
220 else
221 name = node.name;
222
223 // Text node, inline elements are subjected, except for <script>/<style>.
224 return name && name in CKEDITOR.dtd.$inline &&
225 !( name in CKEDITOR.dtd.head ) &&
226 !node.isOrphan ||
227 node.type == CKEDITOR.NODE_TEXT;
228 }
229 }
230
231 // Judge whether two element tag names are likely the siblings from the same
232 // structural element.
233 function possiblySibling( tag1, tag2 ) {
234
235 if ( tag1 in CKEDITOR.dtd.$listItem || tag1 in CKEDITOR.dtd.$tableContent )
236 return tag1 == tag2 || tag1 == 'dt' && tag2 == 'dd' || tag1 == 'dd' && tag2 == 'dt';
237
238 return false;
239 }
240
241 parser.onTagOpen = function( tagName, attributes, selfClosing, optionalClose ) {
242 var element = new CKEDITOR.htmlParser.element( tagName, attributes );
243
244 // "isEmpty" will be always "false" for unknown elements, so we
245 // must force it if the parser has identified it as a selfClosing tag.
246 if ( element.isUnknown && selfClosing )
247 element.isEmpty = true;
248
249 // Check for optional closed elements, including browser quirks and manually opened blocks.
250 element.isOptionalClose = optionalClose;
251
252 // This is a tag to be removed if empty, so do not add it immediately.
253 if ( isRemoveEmpty( element ) ) {
254 pendingInline.push( element );
255 return;
256 } else if ( tagName == 'pre' )
257 inPre = true;
258 else if ( tagName == 'br' && inPre ) {
259 currentNode.add( new CKEDITOR.htmlParser.text( '\n' ) );
260 return;
261 } else if ( tagName == 'textarea' ) {
262 inTextarea = true;
263 }
264
265 if ( tagName == 'br' ) {
266 pendingBRs.push( element );
267 return;
268 }
269
270 while ( 1 ) {
271 var currentName = currentNode.name;
272
273 var currentDtd = currentName ? ( CKEDITOR.dtd[ currentName ] || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ) ) : rootDtd;
274
275 // If the element cannot be child of the current element.
276 if ( !element.isUnknown && !currentNode.isUnknown && !currentDtd[ tagName ] ) {
277 // Current node doesn't have a close tag, time for a close
278 // as this element isn't fit in. (#7497)
279 if ( currentNode.isOptionalClose )
280 parser.onTagClose( currentName );
281 // Fixing malformed nested lists by moving it into a previous list item. (#3828)
282 else if ( tagName in listBlocks && currentName in listBlocks ) {
283 var children = currentNode.children,
284 lastChild = children[ children.length - 1 ];
285
286 // Establish the list item if it's not existed.
287 if ( !( lastChild && lastChild.name == 'li' ) )
288 addElement( ( lastChild = new CKEDITOR.htmlParser.element( 'li' ) ), currentNode );
289
290 !element.returnPoint && ( element.returnPoint = currentNode );
291 currentNode = lastChild;
292 }
293 // Establish new list root for orphan list items, but NOT to create
294 // new list for the following ones, fix them instead. (#6975)
295 // <dl><dt>foo<dd>bar</dl>
296 // <ul><li>foo<li>bar</ul>
297 else if ( tagName in CKEDITOR.dtd.$listItem &&
298 !possiblySibling( tagName, currentName ) ) {
299 parser.onTagOpen( tagName == 'li' ? 'ul' : 'dl', {}, 0, 1 );
300 }
301 // We're inside a structural block like table and list, AND the incoming element
302 // is not of the same type (e.g. <td>td1<td>td2</td>), we simply add this new one before it,
303 // and most importantly, return back to here once this element is added,
304 // e.g. <table><tr><td>td1</td><p>p1</p><td>td2</td></tr></table>
305 else if ( currentName in nonBreakingBlocks &&
306 !possiblySibling( tagName, currentName ) ) {
307 !element.returnPoint && ( element.returnPoint = currentNode );
308 currentNode = currentNode.parent;
309 } else {
310 // The current element is an inline element, which
311 // need to be continued even after the close, so put
312 // it in the pending list.
313 if ( currentName in CKEDITOR.dtd.$inline )
314 pendingInline.unshift( currentNode );
315
316 // The most common case where we just need to close the
317 // current one and append the new one to the parent.
318 if ( currentNode.parent )
319 addElement( currentNode, currentNode.parent, 1 );
320 // We've tried our best to fix the embarrassment here, while
321 // this element still doesn't find it's parent, mark it as
322 // orphan and show our tolerance to it.
323 else {
324 element.isOrphan = 1;
325 break;
326 }
327 }
328 } else {
329 break;
330 }
331 }
332
333 checkPending( tagName );
334 sendPendingBRs();
335
336 element.parent = currentNode;
337
338 if ( element.isEmpty )
339 addElement( element );
340 else
341 currentNode = element;
342 };
343
344 parser.onTagClose = function( tagName ) {
345 // Check if there is any pending tag to be closed.
346 for ( var i = pendingInline.length - 1; i >= 0; i-- ) {
347 // If found, just remove it from the list.
348 if ( tagName == pendingInline[ i ].name ) {
349 pendingInline.splice( i, 1 );
350 return;
351 }
352 }
353
354 var pendingAdd = [],
355 newPendingInline = [],
356 candidate = currentNode;
357
358 while ( candidate != root && candidate.name != tagName ) {
359 // If this is an inline element, add it to the pending list, if we're
360 // really closing one of the parents element later, they will continue
361 // after it.
362 if ( !candidate._.isBlockLike )
363 newPendingInline.unshift( candidate );
364
365 // This node should be added to it's parent at this point. But,
366 // it should happen only if the closing tag is really closing
367 // one of the nodes. So, for now, we just cache it.
368 pendingAdd.push( candidate );
369
370 // Make sure return point is properly restored.
371 candidate = candidate.returnPoint || candidate.parent;
372 }
373
374 if ( candidate != root ) {
375 // Add all elements that have been found in the above loop.
376 for ( i = 0; i < pendingAdd.length; i++ ) {
377 var node = pendingAdd[ i ];
378 addElement( node, node.parent );
379 }
380
381 currentNode = candidate;
382
383 if ( candidate._.isBlockLike )
384 sendPendingBRs();
385
386 addElement( candidate, candidate.parent );
387
388 // The parent should start receiving new nodes now, except if
389 // addElement changed the currentNode.
390 if ( candidate == currentNode )
391 currentNode = currentNode.parent;
392
393 pendingInline = pendingInline.concat( newPendingInline );
394 }
395
396 if ( tagName == 'body' )
397 fixingBlock = false;
398 };
399
400 parser.onText = function( text ) {
401 // Trim empty spaces at beginning of text contents except <pre> and <textarea>.
402 if ( ( !currentNode._.hasInlineStarted || pendingBRs.length ) && !inPre && !inTextarea ) {
403 text = CKEDITOR.tools.ltrim( text );
404
405 if ( text.length === 0 )
406 return;
407 }
408
409 var currentName = currentNode.name,
410 currentDtd = currentName ? ( CKEDITOR.dtd[ currentName ] || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ) ) : rootDtd;
411
412 // Fix orphan text in list/table. (#8540) (#8870)
413 if ( !inTextarea && !currentDtd[ '#' ] && currentName in nonBreakingBlocks ) {
414 parser.onTagOpen( structureFixes[ currentName ] || '' );
415 parser.onText( text );
416 return;
417 }
418
419 sendPendingBRs();
420 checkPending();
421
422 // Shrinking consequential spaces into one single for all elements
423 // text contents.
424 if ( !inPre && !inTextarea )
425 text = text.replace( /[\t\r\n ]{2,}|[\t\r\n]/g, ' ' );
426
427 text = new CKEDITOR.htmlParser.text( text );
428
429
430 if ( checkAutoParagraphing( currentNode, text ) )
431 this.onTagOpen( fixingBlock, {}, 0, 1 );
432
433 currentNode.add( text );
434 };
435
436 parser.onCDATA = function( cdata ) {
437 currentNode.add( new CKEDITOR.htmlParser.cdata( cdata ) );
438 };
439
440 parser.onComment = function( comment ) {
441 sendPendingBRs();
442 checkPending();
443 currentNode.add( new CKEDITOR.htmlParser.comment( comment ) );
444 };
445
446 // Parse it.
447 parser.parse( fragmentHtml );
448
449 sendPendingBRs();
450
451 // Close all pending nodes, make sure return point is properly restored.
452 while ( currentNode != root )
453 addElement( currentNode, currentNode.parent, 1 );
454
455 removeTailWhitespace( root );
456
457 return root;
458 };
459
460 CKEDITOR.htmlParser.fragment.prototype = {
461
462 /**
463 * The node type. This is a constant value set to {@link CKEDITOR#NODE_DOCUMENT_FRAGMENT}.
464 *
465 * @readonly
466 * @property {Number} [=CKEDITOR.NODE_DOCUMENT_FRAGMENT]
467 */
468 type: CKEDITOR.NODE_DOCUMENT_FRAGMENT,
469
470 /**
471 * Adds a node to this fragment.
472 *
473 * @param {CKEDITOR.htmlParser.node} node The node to be added.
474 * @param {Number} [index] From where the insertion happens.
475 */
476 add: function( node, index ) {
477 isNaN( index ) && ( index = this.children.length );
478
479 var previous = index > 0 ? this.children[ index - 1 ] : null;
480 if ( previous ) {
481 // If the block to be appended is following text, trim spaces at
482 // the right of it.
483 if ( node._.isBlockLike && previous.type == CKEDITOR.NODE_TEXT ) {
484 previous.value = CKEDITOR.tools.rtrim( previous.value );
485
486 // If we have completely cleared the previous node.
487 if ( previous.value.length === 0 ) {
488 // Remove it from the list and add the node again.
489 this.children.pop();
490 this.add( node );
491 return;
492 }
493 }
494
495 previous.next = node;
496 }
497
498 node.previous = previous;
499 node.parent = this;
500
501 this.children.splice( index, 0, node );
502
503 if ( !this._.hasInlineStarted )
504 this._.hasInlineStarted = node.type == CKEDITOR.NODE_TEXT || ( node.type == CKEDITOR.NODE_ELEMENT && !node._.isBlockLike );
505 },
506
507 /**
508 * Filter this fragment's content with given filter.
509 *
510 * @since 4.1
511 * @param {CKEDITOR.htmlParser.filter} filter
512 */
513 filter: function( filter, context ) {
514 context = this.getFilterContext( context );
515
516 // Apply the root filter.
517 filter.onRoot( context, this );
518
519 this.filterChildren( filter, false, context );
520 },
521
522 /**
523 * Filter this fragment's children with given filter.
524 *
525 * Element's children may only be filtered once by one
526 * instance of filter.
527 *
528 * @since 4.1
529 * @param {CKEDITOR.htmlParser.filter} filter
530 * @param {Boolean} [filterRoot] Whether to apply the "root" filter rule specified in the `filter`.
531 */
532 filterChildren: function( filter, filterRoot, context ) {
533 // If this element's children were already filtered
534 // by current filter, don't filter them 2nd time.
535 // This situation may occur when filtering bottom-up
536 // (filterChildren() called manually in element's filter),
537 // or in unpredictable edge cases when filter
538 // is manipulating DOM structure.
539 if ( this.childrenFilteredBy == filter.id )
540 return;
541
542 context = this.getFilterContext( context );
543
544 // Filtering root if enforced.
545 if ( filterRoot && !this.parent )
546 filter.onRoot( context, this );
547
548 this.childrenFilteredBy = filter.id;
549
550 // Don't cache anything, children array may be modified by filter rule.
551 for ( var i = 0; i < this.children.length; i++ ) {
552 // Stay in place if filter returned false, what means
553 // that node has been removed.
554 if ( this.children[ i ].filter( filter, context ) === false )
555 i--;
556 }
557 },
558
559 /**
560 * Writes the fragment HTML to a {@link CKEDITOR.htmlParser.basicWriter}.
561 *
562 * var writer = new CKEDITOR.htmlWriter();
563 * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<P><B>Example' );
564 * fragment.writeHtml( writer );
565 * alert( writer.getHtml() ); // '<p><b>Example</b></p>'
566 *
567 * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML.
568 * @param {CKEDITOR.htmlParser.filter} [filter] The filter to use when writing the HTML.
569 */
570 writeHtml: function( writer, filter ) {
571 if ( filter )
572 this.filter( filter );
573
574 this.writeChildrenHtml( writer );
575 },
576
577 /**
578 * Write and filtering the child nodes of this fragment.
579 *
580 * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML.
581 * @param {CKEDITOR.htmlParser.filter} [filter] The filter to use when writing the HTML.
582 * @param {Boolean} [filterRoot] Whether to apply the "root" filter rule specified in the `filter`.
583 */
584 writeChildrenHtml: function( writer, filter, filterRoot ) {
585 var context = this.getFilterContext();
586
587 // Filtering root if enforced.
588 if ( filterRoot && !this.parent && filter )
589 filter.onRoot( context, this );
590
591 if ( filter )
592 this.filterChildren( filter, false, context );
593
594 for ( var i = 0, children = this.children, l = children.length; i < l; i++ )
595 children[ i ].writeHtml( writer );
596 },
597
598 /**
599 * Execute callback on each node (of given type) in this document fragment.
600 *
601 * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<p>foo<b>bar</b>bom</p>' );
602 * fragment.forEach( function( node ) {
603 * console.log( node );
604 * } );
605 * // Will log:
606 * // 1. document fragment,
607 * // 2. <p> element,
608 * // 3. "foo" text node,
609 * // 4. <b> element,
610 * // 5. "bar" text node,
611 * // 6. "bom" text node.
612 *
613 * @since 4.1
614 * @param {Function} callback Function to be executed on every node.
615 * **Since 4.3** if `callback` returned `false` descendants of current node will be ignored.
616 * @param {CKEDITOR.htmlParser.node} callback.node Node passed as argument.
617 * @param {Number} [type] If specified `callback` will be executed only on nodes of this type.
618 * @param {Boolean} [skipRoot] Don't execute `callback` on this fragment.
619 */
620 forEach: function( callback, type, skipRoot ) {
621 if ( !skipRoot && ( !type || this.type == type ) )
622 var ret = callback( this );
623
624 // Do not filter children if callback returned false.
625 if ( ret === false )
626 return;
627
628 var children = this.children,
629 node,
630 i = 0;
631
632 // We do not cache the size, because the list of nodes may be changed by the callback.
633 for ( ; i < children.length; i++ ) {
634 node = children[ i ];
635 if ( node.type == CKEDITOR.NODE_ELEMENT )
636 node.forEach( callback, type );
637 else if ( !type || node.type == type )
638 callback( node );
639 }
640 },
641
642 getFilterContext: function( context ) {
643 return context || {};
644 }
645 };
646} )();
diff --git a/sources/core/htmlparser/node.js b/sources/core/htmlparser/node.js
new file mode 100644
index 0000000..b38c8a8
--- /dev/null
+++ b/sources/core/htmlparser/node.js
@@ -0,0 +1,156 @@
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'use strict';
7
8( function() {
9 /**
10 * A lightweight representation of HTML node.
11 *
12 * @since 4.1
13 * @class
14 * @constructor Creates a node class instance.
15 */
16 CKEDITOR.htmlParser.node = function() {};
17
18 CKEDITOR.htmlParser.node.prototype = {
19 /**
20 * Remove this node from a tree.
21 *
22 * @since 4.1
23 */
24 remove: function() {
25 var children = this.parent.children,
26 index = CKEDITOR.tools.indexOf( children, this ),
27 previous = this.previous,
28 next = this.next;
29
30 previous && ( previous.next = next );
31 next && ( next.previous = previous );
32 children.splice( index, 1 );
33 this.parent = null;
34 },
35
36 /**
37 * Replace this node with given one.
38 *
39 * @since 4.1
40 * @param {CKEDITOR.htmlParser.node} node The node that will replace this one.
41 */
42 replaceWith: function( node ) {
43 var children = this.parent.children,
44 index = CKEDITOR.tools.indexOf( children, this ),
45 previous = node.previous = this.previous,
46 next = node.next = this.next;
47
48 previous && ( previous.next = node );
49 next && ( next.previous = node );
50
51 children[ index ] = node;
52
53 node.parent = this.parent;
54 this.parent = null;
55 },
56
57 /**
58 * Insert this node after given one.
59 *
60 * @since 4.1
61 * @param {CKEDITOR.htmlParser.node} node The node that will precede this element.
62 */
63 insertAfter: function( node ) {
64 var children = node.parent.children,
65 index = CKEDITOR.tools.indexOf( children, node ),
66 next = node.next;
67
68 children.splice( index + 1, 0, this );
69
70 this.next = node.next;
71 this.previous = node;
72 node.next = this;
73 next && ( next.previous = this );
74
75 this.parent = node.parent;
76 },
77
78 /**
79 * Insert this node before given one.
80 *
81 * @since 4.1
82 * @param {CKEDITOR.htmlParser.node} node The node that will follow this element.
83 */
84 insertBefore: function( node ) {
85 var children = node.parent.children,
86 index = CKEDITOR.tools.indexOf( children, node );
87
88 children.splice( index, 0, this );
89
90 this.next = node;
91 this.previous = node.previous;
92 node.previous && ( node.previous.next = this );
93 node.previous = this;
94
95 this.parent = node.parent;
96 },
97
98 /**
99 * Gets the closest ancestor element of this element which satisfies given condition
100 *
101 * @since 4.3
102 * @param {String/Object/Function} condition Name of an ancestor, hash of names or validator function.
103 * @returns {CKEDITOR.htmlParser.element} The closest ancestor which satisfies given condition or `null`.
104 */
105 getAscendant: function( condition ) {
106 var checkFn =
107 typeof condition == 'function' ?
108 condition :
109 typeof condition == 'string' ?
110 function( el ) {
111 return el.name == condition;
112 } :
113 function( el ) {
114 return el.name in condition;
115 };
116
117 var parent = this.parent;
118
119 // Parent has to be an element - don't check doc fragment.
120 while ( parent && parent.type == CKEDITOR.NODE_ELEMENT ) {
121 if ( checkFn( parent ) )
122 return parent;
123 parent = parent.parent;
124 }
125
126 return null;
127 },
128
129 /**
130 * Wraps this element with given `wrapper`.
131 *
132 * @since 4.3
133 * @param {CKEDITOR.htmlParser.element} wrapper The element which will be this element's new parent.
134 * @returns {CKEDITOR.htmlParser.element} Wrapper.
135 */
136 wrapWith: function( wrapper ) {
137 this.replaceWith( wrapper );
138 wrapper.add( this );
139 return wrapper;
140 },
141
142 /**
143 * Gets this node's index in its parent's children array.
144 *
145 * @since 4.3
146 * @returns {Number}
147 */
148 getIndex: function() {
149 return CKEDITOR.tools.indexOf( this.parent.children, this );
150 },
151
152 getFilterContext: function( context ) {
153 return context || {};
154 }
155 };
156} )();
diff --git a/sources/core/htmlparser/text.js b/sources/core/htmlparser/text.js
new file mode 100644
index 0000000..190e85a
--- /dev/null
+++ b/sources/core/htmlparser/text.js
@@ -0,0 +1,70 @@
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 'use strict';
7
8( function() {
9 /**
10 * A lightweight representation of HTML text.
11 *
12 * @class
13 * @extends CKEDITOR.htmlParser.node
14 * @constructor Creates a text class instance.
15 * @param {String} value The text node value.
16 */
17 CKEDITOR.htmlParser.text = function( value ) {
18 /**
19 * The text value.
20 *
21 * @property {String}
22 */
23 this.value = value;
24
25 /** @private */
26 this._ = {
27 isBlockLike: false
28 };
29 };
30
31 CKEDITOR.htmlParser.text.prototype = CKEDITOR.tools.extend( new CKEDITOR.htmlParser.node(), {
32 /**
33 * The node type. This is a constant value set to {@link CKEDITOR#NODE_TEXT}.
34 *
35 * @readonly
36 * @property {Number} [=CKEDITOR.NODE_TEXT]
37 */
38 type: CKEDITOR.NODE_TEXT,
39
40 /**
41 * Filter this text node with given filter.
42 *
43 * @since 4.1
44 * @param {CKEDITOR.htmlParser.filter} filter
45 * @returns {Boolean} Method returns `false` when this text node has
46 * been removed. This is an information for {@link CKEDITOR.htmlParser.element#filterChildren}
47 * that it has to repeat filter on current position in parent's children array.
48 */
49 filter: function( filter, context ) {
50 if ( !( this.value = filter.onText( context, this.value, this ) ) ) {
51 this.remove();
52 return false;
53 }
54 },
55
56 /**
57 * Writes the HTML representation of this text to a {CKEDITOR.htmlParser.basicWriter}.
58 *
59 * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML.
60 * @param {CKEDITOR.htmlParser.filter} [filter] The filter to be applied to this node.
61 * **Note:** it's unsafe to filter offline (not appended) node.
62 */
63 writeHtml: function( writer, filter ) {
64 if ( filter )
65 this.filter( filter );
66
67 writer.text( this.value );
68 }
69 } );
70} )();