]>
Commit | Line | Data |
---|---|---|
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 | */ |