diff options
Diffstat (limited to 'sources/core/htmlparser/filter.js')
-rw-r--r-- | sources/core/htmlparser/filter.js | 407 |
1 files changed, 407 insertions, 0 deletions
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 | */ | ||