diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2016-02-19 23:38:52 +0100 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2016-02-19 23:38:52 +0100 |
commit | 3332bebe4da6dfa0fe3e4b2abddc84b1cc62f8f5 (patch) | |
tree | a4f77655fe55b79606e7d3416504686a1ab8b058 /sources/core/htmlparser/element.js | |
download | piedsjaloux-ckeditor-component-3332bebe4da6dfa0fe3e4b2abddc84b1cc62f8f5.tar.gz piedsjaloux-ckeditor-component-3332bebe4da6dfa0fe3e4b2abddc84b1cc62f8f5.tar.zst piedsjaloux-ckeditor-component-3332bebe4da6dfa0fe3e4b2abddc84b1cc62f8f5.zip |
Initial commit4.5.7
Diffstat (limited to 'sources/core/htmlparser/element.js')
-rw-r--r-- | sources/core/htmlparser/element.js | 536 |
1 files changed, 536 insertions, 0 deletions
diff --git a/sources/core/htmlparser/element.js b/sources/core/htmlparser/element.js new file mode 100644 index 0000000..3654322 --- /dev/null +++ b/sources/core/htmlparser/element.js | |||
@@ -0,0 +1,536 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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 | */ | ||
18 | CKEDITOR.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 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 | */ | ||
66 | CKEDITOR.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: "Lucida, Console"' | ||
76 | // TODO reuse CSS methods from tools. | ||
77 | ( styleText || '' ).replace( /"/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 — `0` means the beginning, | ||
422 | * `1` after 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 | * Adds a class name to the list of classes. | ||
447 | * | ||
448 | * @since 4.4 | ||
449 | * @param {String} className The class name to be added. | ||
450 | */ | ||
451 | addClass: function( className ) { | ||
452 | if ( this.hasClass( className ) ) | ||
453 | return; | ||
454 | |||
455 | var c = this.attributes[ 'class' ] || ''; | ||
456 | |||
457 | this.attributes[ 'class' ] = c + ( c ? ' ' : '' ) + className; | ||
458 | }, | ||
459 | |||
460 | /** | ||
461 | * Removes a class name from the list of classes. | ||
462 | * | ||
463 | * @since 4.3 | ||
464 | * @param {String} className The class name to be removed. | ||
465 | */ | ||
466 | removeClass: function( className ) { | ||
467 | var classes = this.attributes[ 'class' ]; | ||
468 | |||
469 | if ( !classes ) | ||
470 | return; | ||
471 | |||
472 | // We can safely assume that className won't break regexp. | ||
473 | // http://stackoverflow.com/questions/448981/what-characters-are-valid-in-css-class-names | ||
474 | classes = CKEDITOR.tools.trim( classes.replace( new RegExp( '(?:\\s+|^)' + className + '(?:\\s+|$)' ), ' ' ) ); | ||
475 | |||
476 | if ( classes ) | ||
477 | this.attributes[ 'class' ] = classes; | ||
478 | else | ||
479 | delete this.attributes[ 'class' ]; | ||
480 | }, | ||
481 | |||
482 | /** | ||
483 | * Checkes whether this element has a class name. | ||
484 | * | ||
485 | * @since 4.3 | ||
486 | * @param {String} className The class name to be checked. | ||
487 | * @returns {Boolean} Whether this element has a `className`. | ||
488 | */ | ||
489 | hasClass: function( className ) { | ||
490 | var classes = this.attributes[ 'class' ]; | ||
491 | |||
492 | if ( !classes ) | ||
493 | return false; | ||
494 | |||
495 | return ( new RegExp( '(?:^|\\s)' + className + '(?=\\s|$)' ) ).test( classes ); | ||
496 | }, | ||
497 | |||
498 | getFilterContext: function( ctx ) { | ||
499 | var changes = []; | ||
500 | |||
501 | if ( !ctx ) { | ||
502 | ctx = { | ||
503 | off: false, | ||
504 | nonEditable: false, | ||
505 | nestedEditable: false | ||
506 | }; | ||
507 | } | ||
508 | |||
509 | if ( !ctx.off && this.attributes[ 'data-cke-processor' ] == 'off' ) | ||
510 | changes.push( 'off', true ); | ||
511 | |||
512 | if ( !ctx.nonEditable && this.attributes.contenteditable == 'false' ) | ||
513 | changes.push( 'nonEditable', true ); | ||
514 | // A context to be given nestedEditable must be nonEditable first (by inheritance) (#11372, #11698). | ||
515 | // Special case: #11504 - filter starts on <body contenteditable=true>, | ||
516 | // so ctx.nonEditable has not been yet set to true. | ||
517 | else if ( ctx.nonEditable && !ctx.nestedEditable && this.attributes.contenteditable == 'true' ) | ||
518 | changes.push( 'nestedEditable', true ); | ||
519 | |||
520 | if ( changes.length ) { | ||
521 | ctx = CKEDITOR.tools.copy( ctx ); | ||
522 | for ( var i = 0; i < changes.length; i += 2 ) | ||
523 | ctx[ changes[ i ] ] = changes[ i + 1 ]; | ||
524 | } | ||
525 | |||
526 | return ctx; | ||
527 | } | ||
528 | }, true ); | ||
529 | |||
530 | function nameCondition( condition ) { | ||
531 | return function( el ) { | ||
532 | return el.type == CKEDITOR.NODE_ELEMENT && | ||
533 | ( typeof condition == 'string' ? el.name == condition : el.name in condition ); | ||
534 | }; | ||
535 | } | ||
536 | } )(); | ||