]>
Commit | Line | Data |
---|---|---|
c63493c8 IB |
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 | */ | |
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 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 | */ | |
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 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 | } )(); |