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 | |
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')
-rw-r--r-- | sources/core/htmlparser/basicwriter.js | 152 | ||||
-rw-r--r-- | sources/core/htmlparser/cdata.js | 48 | ||||
-rw-r--r-- | sources/core/htmlparser/comment.js | 80 | ||||
-rw-r--r-- | sources/core/htmlparser/element.js | 536 | ||||
-rw-r--r-- | sources/core/htmlparser/filter.js | 407 | ||||
-rw-r--r-- | sources/core/htmlparser/fragment.js | 646 | ||||
-rw-r--r-- | sources/core/htmlparser/node.js | 156 | ||||
-rw-r--r-- | sources/core/htmlparser/text.js | 70 |
8 files changed, 2095 insertions, 0 deletions
diff --git a/sources/core/htmlparser/basicwriter.js b/sources/core/htmlparser/basicwriter.js new file mode 100644 index 0000000..62a97ef --- /dev/null +++ b/sources/core/htmlparser/basicwriter.js | |||
@@ -0,0 +1,152 @@ | |||
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 | /** | ||
7 | * TODO | ||
8 | * | ||
9 | * @class | ||
10 | * @todo | ||
11 | */ | ||
12 | CKEDITOR.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..4ece2b7 --- /dev/null +++ b/sources/core/htmlparser/cdata.js | |||
@@ -0,0 +1,48 @@ | |||
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 | ( 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..171c62e --- /dev/null +++ b/sources/core/htmlparser/comment.js | |||
@@ -0,0 +1,80 @@ | |||
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 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 | */ | ||
16 | CKEDITOR.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 | |||
30 | CKEDITOR.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..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 | } )(); | ||
diff --git a/sources/core/htmlparser/filter.js b/sources/core/htmlparser/filter.js new file mode 100644 index 0000000..72767b5 --- /dev/null +++ b/sources/core/htmlparser/filter.js | |||
@@ -0,0 +1,407 @@ | |||
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 | ( 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..c062986 --- /dev/null +++ b/sources/core/htmlparser/fragment.js | |||
@@ -0,0 +1,646 @@ | |||
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 DOM structure. | ||
10 | * | ||
11 | * @class | ||
12 | * @constructor Creates a fragment class instance. | ||
13 | */ | ||
14 | CKEDITOR.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..0f1b307 --- /dev/null +++ b/sources/core/htmlparser/node.js | |||
@@ -0,0 +1,156 @@ | |||
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 | ( 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..07cb865 --- /dev/null +++ b/sources/core/htmlparser/text.js | |||
@@ -0,0 +1,70 @@ | |||
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 | ( 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 | } )(); | ||