Version 1
[yaffs-website] / web / core / modules / editor / js / editor.admin.js
1 /**
2  * @file
3  * Provides a JavaScript API to broadcast text editor configuration changes.
4  *
5  * Filter implementations may listen to the drupalEditorFeatureAdded,
6  * drupalEditorFeatureRemoved, and drupalEditorFeatureRemoved events on document
7  * to automatically adjust their settings based on the editor configuration.
8  */
9
10 (function ($, _, Drupal, document) {
11
12   'use strict';
13
14   /**
15    * Editor configuration namespace.
16    *
17    * @namespace
18    */
19   Drupal.editorConfiguration = {
20
21     /**
22      * Must be called by a specific text editor's configuration whenever a
23      * feature is added by the user.
24      *
25      * Triggers the drupalEditorFeatureAdded event on the document, which
26      * receives a {@link Drupal.EditorFeature} object.
27      *
28      * @param {Drupal.EditorFeature} feature
29      *   A text editor feature object.
30      *
31      * @fires event:drupalEditorFeatureAdded
32      */
33     addedFeature: function (feature) {
34       $(document).trigger('drupalEditorFeatureAdded', feature);
35     },
36
37     /**
38      * Must be called by a specific text editor's configuration whenever a
39      * feature is removed by the user.
40      *
41      * Triggers the drupalEditorFeatureRemoved event on the document, which
42      * receives a {@link Drupal.EditorFeature} object.
43      *
44      * @param {Drupal.EditorFeature} feature
45      *   A text editor feature object.
46      *
47      * @fires event:drupalEditorFeatureRemoved
48      */
49     removedFeature: function (feature) {
50       $(document).trigger('drupalEditorFeatureRemoved', feature);
51     },
52
53     /**
54      * Must be called by a specific text editor's configuration whenever a
55      * feature is modified, i.e. has different rules.
56      *
57      * For example when the "Bold" button is configured to use the `<b>` tag
58      * instead of the `<strong>` tag.
59      *
60      * Triggers the drupalEditorFeatureModified event on the document, which
61      * receives a {@link Drupal.EditorFeature} object.
62      *
63      * @param {Drupal.EditorFeature} feature
64      *   A text editor feature object.
65      *
66      * @fires event:drupalEditorFeatureModified
67      */
68     modifiedFeature: function (feature) {
69       $(document).trigger('drupalEditorFeatureModified', feature);
70     },
71
72     /**
73      * May be called by a specific text editor's configuration whenever a
74      * feature is being added, to check whether it would require the filter
75      * settings to be updated.
76      *
77      * The canonical use case is when a text editor is being enabled:
78      * preferably
79      * this would not cause the filter settings to be changed; rather, the
80      * default set of buttons (features) for the text editor should adjust
81      * itself to not cause filter setting changes.
82      *
83      * Note: for filters to integrate with this functionality, it is necessary
84      * that they implement
85      * `Drupal.filterSettingsForEditors[filterID].getRules()`.
86      *
87      * @param {Drupal.EditorFeature} feature
88      *   A text editor feature object.
89      *
90      * @return {bool}
91      *   Whether the given feature is allowed by the current filters.
92      */
93     featureIsAllowedByFilters: function (feature) {
94
95       /**
96        * Generate the universe U of possible values that can result from the
97        * feature's rules' requirements.
98        *
99        * This generates an object of this form:
100        *   var universe = {
101        *     a: {
102        *       'touchedByAllowedPropertyRule': false,
103        *       'tag': false,
104        *       'attributes:href': false,
105        *       'classes:external': false,
106        *     },
107        *     strong: {
108        *       'touchedByAllowedPropertyRule': false,
109        *       'tag': false,
110        *     },
111        *     img: {
112        *       'touchedByAllowedPropertyRule': false,
113        *       'tag': false,
114        *       'attributes:src': false
115        *     }
116        *   };
117        *
118        * In this example, the given text editor feature resulted in the above
119        * universe, which shows that it must be allowed to generate the a,
120        * strong and img tags. For the a tag, it must be able to set the "href"
121        * attribute and the "external" class. For the strong tag, no further
122        * properties are required. For the img tag, the "src" attribute is
123        * required. The "tag" key is used to track whether that tag was
124        * explicitly allowed by one of the filter's rules. The
125        * "touchedByAllowedPropertyRule" key is used for state tracking that is
126        * essential for filterStatusAllowsFeature() to be able to reason: when
127        * all of a filter's rules have been applied, and none of the forbidden
128        * rules matched (which would have resulted in early termination) yet the
129        * universe has not been made empty (which would be the end result if
130        * everything in the universe were explicitly allowed), then this piece
131        * of state data enables us to determine whether a tag whose properties
132        * were not all explicitly allowed are in fact still allowed, because its
133        * tag was explicitly allowed and there were no filter rules applying
134        * "allowed tag property value" restrictions for this particular tag.
135        *
136        * @param {object} feature
137        *   The feature in question.
138        *
139        * @return {object}
140        *   The universe generated.
141        *
142        * @see findPropertyValueOnTag()
143        * @see filterStatusAllowsFeature()
144        */
145       function generateUniverseFromFeatureRequirements(feature) {
146         var properties = ['attributes', 'styles', 'classes'];
147         var universe = {};
148
149         for (var r = 0; r < feature.rules.length; r++) {
150           var featureRule = feature.rules[r];
151
152           // For each tag required by this feature rule, create a basic entry in
153           // the universe.
154           var requiredTags = featureRule.required.tags;
155           for (var t = 0; t < requiredTags.length; t++) {
156             universe[requiredTags[t]] = {
157               // Whether this tag was allowed or not.
158               tag: false,
159               // Whether any filter rule that applies to this tag had an allowed
160               // property rule. i.e. will become true if >=1 filter rule has >=1
161               // allowed property rule.
162               touchedByAllowedPropertyRule: false,
163               // Analogous, but for forbidden property rule.
164               touchedBytouchedByForbiddenPropertyRule: false
165             };
166           }
167
168           // If no required properties are defined for this rule, we can move on
169           // to the next feature.
170           if (emptyProperties(featureRule.required)) {
171             continue;
172           }
173
174           // Expand the existing universe, assume that each tags' property
175           // value is disallowed. If the filter rules allow everything in the
176           // feature's universe, then the feature is allowed.
177           for (var p = 0; p < properties.length; p++) {
178             var property = properties[p];
179             for (var pv = 0; pv < featureRule.required[property].length; pv++) {
180               var propertyValue = featureRule.required[property];
181               universe[requiredTags][property + ':' + propertyValue] = false;
182             }
183           }
184         }
185
186         return universe;
187       }
188
189       /**
190        * Provided a section of a feature or filter rule, checks if no property
191        * values are defined for all properties: attributes, classes and styles.
192        *
193        * @param {object} section
194        *   The section to check.
195        *
196        * @return {bool}
197        *   Returns true if the section has empty properties, false otherwise.
198        */
199       function emptyProperties(section) {
200         return section.attributes.length === 0 && section.classes.length === 0 && section.styles.length === 0;
201       }
202
203       /**
204        * Calls findPropertyValueOnTag on the given tag for every property value
205        * that is listed in the "propertyValues" parameter. Supports the wildcard
206        * tag.
207        *
208        * @param {object} universe
209        *   The universe to check.
210        * @param {string} tag
211        *   The tag to look for.
212        * @param {string} property
213        *   The property to check.
214        * @param {Array} propertyValues
215        *   Values of the property to check.
216        * @param {bool} allowing
217        *   Whether to update the universe or not.
218        *
219        * @return {bool}
220        *   Returns true if found, false otherwise.
221        */
222       function findPropertyValuesOnTag(universe, tag, property, propertyValues, allowing) {
223         // Detect the wildcard case.
224         if (tag === '*') {
225           return findPropertyValuesOnAllTags(universe, property, propertyValues, allowing);
226         }
227
228         var atLeastOneFound = false;
229         _.each(propertyValues, function (propertyValue) {
230           if (findPropertyValueOnTag(universe, tag, property, propertyValue, allowing)) {
231             atLeastOneFound = true;
232           }
233         });
234         return atLeastOneFound;
235       }
236
237       /**
238        * Calls findPropertyValuesOnAllTags for all tags in the universe.
239        *
240        * @param {object} universe
241        *   The universe to check.
242        * @param {string} property
243        *   The property to check.
244        * @param {Array} propertyValues
245        *   Values of the property to check.
246        * @param {bool} allowing
247        *   Whether to update the universe or not.
248        *
249        * @return {bool}
250        *   Returns true if found, false otherwise.
251        */
252       function findPropertyValuesOnAllTags(universe, property, propertyValues, allowing) {
253         var atLeastOneFound = false;
254         _.each(_.keys(universe), function (tag) {
255           if (findPropertyValuesOnTag(universe, tag, property, propertyValues, allowing)) {
256             atLeastOneFound = true;
257           }
258         });
259         return atLeastOneFound;
260       }
261
262       /**
263        * Finds out if a specific property value (potentially containing
264        * wildcards) exists on the given tag. When the "allowing" parameter
265        * equals true, the universe will be updated if that specific property
266        * value exists. Returns true if found, false otherwise.
267        *
268        * @param {object} universe
269        *   The universe to check.
270        * @param {string} tag
271        *   The tag to look for.
272        * @param {string} property
273        *   The property to check.
274        * @param {string} propertyValue
275        *   The property value to check.
276        * @param {bool} allowing
277        *   Whether to update the universe or not.
278        *
279        * @return {bool}
280        *   Returns true if found, false otherwise.
281        */
282       function findPropertyValueOnTag(universe, tag, property, propertyValue, allowing) {
283         // If the tag does not exist in the universe, then it definitely can't
284         // have this specific property value.
285         if (!_.has(universe, tag)) {
286           return false;
287         }
288
289         var key = property + ':' + propertyValue;
290
291         // Track whether a tag was touched by a filter rule that allows specific
292         // property values on this particular tag.
293         // @see generateUniverseFromFeatureRequirements
294         if (allowing) {
295           universe[tag].touchedByAllowedPropertyRule = true;
296         }
297
298         // The simple case: no wildcard in property value.
299         if (_.indexOf(propertyValue, '*') === -1) {
300           if (_.has(universe, tag) && _.has(universe[tag], key)) {
301             if (allowing) {
302               universe[tag][key] = true;
303             }
304             return true;
305           }
306           return false;
307         }
308         // The complex case: wildcard in property value.
309         else {
310           var atLeastOneFound = false;
311           var regex = key.replace(/\*/g, '[^ ]*');
312           _.each(_.keys(universe[tag]), function (key) {
313             if (key.match(regex)) {
314               atLeastOneFound = true;
315               if (allowing) {
316                 universe[tag][key] = true;
317               }
318             }
319           });
320           return atLeastOneFound;
321         }
322       }
323
324       /**
325        * Deletes a tag from the universe if the tag itself and each of its
326        * properties are marked as allowed.
327        *
328        * @param {object} universe
329        *   The universe to delete from.
330        * @param {string} tag
331        *   The tag to check.
332        *
333        * @return {bool}
334        *   Whether something was deleted from the universe.
335        */
336       function deleteFromUniverseIfAllowed(universe, tag) {
337         // Detect the wildcard case.
338         if (tag === '*') {
339           return deleteAllTagsFromUniverseIfAllowed(universe);
340         }
341         if (_.has(universe, tag) && _.every(_.omit(universe[tag], 'touchedByAllowedPropertyRule'))) {
342           delete universe[tag];
343           return true;
344         }
345         return false;
346       }
347
348       /**
349        * Calls deleteFromUniverseIfAllowed for all tags in the universe.
350        *
351        * @param {object} universe
352        *   The universe to delete from.
353        *
354        * @return {bool}
355        *   Whether something was deleted from the universe.
356        */
357       function deleteAllTagsFromUniverseIfAllowed(universe) {
358         var atLeastOneDeleted = false;
359         _.each(_.keys(universe), function (tag) {
360           if (deleteFromUniverseIfAllowed(universe, tag)) {
361             atLeastOneDeleted = true;
362           }
363         });
364         return atLeastOneDeleted;
365       }
366
367       /**
368        * Checks if any filter rule forbids either a tag or a tag property value
369        * that exists in the universe.
370        *
371        * @param {object} universe
372        *   Universe to check.
373        * @param {object} filterStatus
374        *   Filter status to use for check.
375        *
376        * @return {bool}
377        *   Whether any filter rule forbids something in the universe.
378        */
379       function anyForbiddenFilterRuleMatches(universe, filterStatus) {
380         var properties = ['attributes', 'styles', 'classes'];
381
382         // Check if a tag in the universe is forbidden.
383         var allRequiredTags = _.keys(universe);
384         var filterRule;
385         for (var i = 0; i < filterStatus.rules.length; i++) {
386           filterRule = filterStatus.rules[i];
387           if (filterRule.allow === false) {
388             if (_.intersection(allRequiredTags, filterRule.tags).length > 0) {
389               return true;
390             }
391           }
392         }
393
394         // Check if a property value of a tag in the universe is forbidden.
395         // For all filter rules…
396         for (var n = 0; n < filterStatus.rules.length; n++) {
397           filterRule = filterStatus.rules[n];
398           // â€¦ if there are tags with restricted property values â€¦
399           if (filterRule.restrictedTags.tags.length && !emptyProperties(filterRule.restrictedTags.forbidden)) {
400             // â€¦ for all those tags â€¦
401             for (var j = 0; j < filterRule.restrictedTags.tags.length; j++) {
402               var tag = filterRule.restrictedTags.tags[j];
403               // â€¦ then iterate over all properties â€¦
404               for (var k = 0; k < properties.length; k++) {
405                 var property = properties[k];
406                 // â€¦ and return true if just one of the forbidden property
407                 // values for this tag and property is listed in the universe.
408                 if (findPropertyValuesOnTag(universe, tag, property, filterRule.restrictedTags.forbidden[property], false)) {
409                   return true;
410                 }
411               }
412             }
413           }
414         }
415
416         return false;
417       }
418
419       /**
420        * Applies every filter rule's explicit allowing of a tag or a tag
421        * property value to the universe. Whenever both the tag and all of its
422        * required property values are marked as explicitly allowed, they are
423        * deleted from the universe.
424        *
425        * @param {object} universe
426        *   Universe to delete from.
427        * @param {object} filterStatus
428        *   The filter status in question.
429        */
430       function markAllowedTagsAndPropertyValues(universe, filterStatus) {
431         var properties = ['attributes', 'styles', 'classes'];
432
433         // Check if a tag in the universe is allowed.
434         var filterRule;
435         var tag;
436         for (var l = 0; !_.isEmpty(universe) && l < filterStatus.rules.length; l++) {
437           filterRule = filterStatus.rules[l];
438           if (filterRule.allow === true) {
439             for (var m = 0; !_.isEmpty(universe) && m < filterRule.tags.length; m++) {
440               tag = filterRule.tags[m];
441               if (_.has(universe, tag)) {
442                 universe[tag].tag = true;
443                 deleteFromUniverseIfAllowed(universe, tag);
444               }
445             }
446           }
447         }
448
449         // Check if a property value of a tag in the universe is allowed.
450         // For all filter rules…
451         for (var i = 0; !_.isEmpty(universe) && i < filterStatus.rules.length; i++) {
452           filterRule = filterStatus.rules[i];
453           // â€¦ if there are tags with restricted property values â€¦
454           if (filterRule.restrictedTags.tags.length && !emptyProperties(filterRule.restrictedTags.allowed)) {
455             // â€¦ for all those tags â€¦
456             for (var j = 0; !_.isEmpty(universe) && j < filterRule.restrictedTags.tags.length; j++) {
457               tag = filterRule.restrictedTags.tags[j];
458               // â€¦ then iterate over all properties â€¦
459               for (var k = 0; k < properties.length; k++) {
460                 var property = properties[k];
461                 // â€¦ and try to delete this tag from the universe if just one
462                 // of the allowed property values for this tag and property is
463                 // listed in the universe. (Because everything might be allowed
464                 // now.)
465                 if (findPropertyValuesOnTag(universe, tag, property, filterRule.restrictedTags.allowed[property], true)) {
466                   deleteFromUniverseIfAllowed(universe, tag);
467                 }
468               }
469             }
470           }
471         }
472       }
473
474       /**
475        * Checks whether the current status of a filter allows a specific feature
476        * by building the universe of potential values from the feature's
477        * requirements and then checking whether anything in the filter prevents
478        * that.
479        *
480        * @param {object} filterStatus
481        *   The filter status in question.
482        * @param {object} feature
483        *   The feature requested.
484        *
485        * @return {bool}
486        *   Whether the current status of the filter allows specified feature.
487        *
488        * @see generateUniverseFromFeatureRequirements()
489        */
490       function filterStatusAllowsFeature(filterStatus, feature) {
491         // An inactive filter by definition allows the feature.
492         if (!filterStatus.active) {
493           return true;
494         }
495
496         // A feature that specifies no rules has no HTML requirements and is
497         // hence allowed by definition.
498         if (feature.rules.length === 0) {
499           return true;
500         }
501
502         // Analogously for a filter that specifies no rules.
503         if (filterStatus.rules.length === 0) {
504           return true;
505         }
506
507         // Generate the universe U of possible values that can result from the
508         // feature's rules' requirements.
509         var universe = generateUniverseFromFeatureRequirements(feature);
510
511         // If anything that is in the universe (and is thus required by the
512         // feature) is forbidden by any of the filter's rules, then this filter
513         // does not allow this feature.
514         if (anyForbiddenFilterRuleMatches(universe, filterStatus)) {
515           return false;
516         }
517
518         // Mark anything in the universe that is allowed by any of the filter's
519         // rules as allowed. If everything is explicitly allowed, then the
520         // universe will become empty.
521         markAllowedTagsAndPropertyValues(universe, filterStatus);
522
523         // If there was at least one filter rule allowing tags, then everything
524         // in the universe must be allowed for this feature to be allowed, and
525         // thus by now it must be empty. However, it is still possible that the
526         // filter allows the feature, due to no rules for allowing tag property
527         // values and/or rules for forbidding tag property values. For details:
528         // see the comments below.
529         // @see generateUniverseFromFeatureRequirements()
530         if (_.some(_.pluck(filterStatus.rules, 'allow'))) {
531           // If the universe is empty, then everything was explicitly allowed
532           // and our job is done: this filter allows this feature!
533           if (_.isEmpty(universe)) {
534             return true;
535           }
536           // Otherwise, it is still possible that this feature is allowed.
537           else {
538             // Every tag must be explicitly allowed if there are filter rules
539             // doing tag whitelisting.
540             if (!_.every(_.pluck(universe, 'tag'))) {
541               return false;
542             }
543             // Every tag was explicitly allowed, but since the universe is not
544             // empty, one or more tag properties are disallowed. However, if
545             // only blacklisting of tag properties was applied to these tags,
546             // and no whitelisting was ever applied, then it's still fine:
547             // since none of the tag properties were blacklisted, we got to
548             // this point, and since no whitelisting was applied, it doesn't
549             // matter that the properties: this could never have happened
550             // anyway. It's only this late that we can know this for certain.
551             else {
552               var tags = _.keys(universe);
553               // Figure out if there was any rule applying whitelisting tag
554               // restrictions to each of the remaining tags.
555               for (var i = 0; i < tags.length; i++) {
556                 var tag = tags[i];
557                 if (_.has(universe, tag)) {
558                   if (universe[tag].touchedByAllowedPropertyRule === false) {
559                     delete universe[tag];
560                   }
561                 }
562               }
563               return _.isEmpty(universe);
564             }
565           }
566         }
567         // Otherwise, if all filter rules were doing blacklisting, then the sole
568         // fact that we got to this point indicates that this filter allows for
569         // everything that is required for this feature.
570         else {
571           return true;
572         }
573       }
574
575       // If any filter's current status forbids the editor feature, return
576       // false.
577       Drupal.filterConfiguration.update();
578       for (var filterID in Drupal.filterConfiguration.statuses) {
579         if (Drupal.filterConfiguration.statuses.hasOwnProperty(filterID)) {
580           var filterStatus = Drupal.filterConfiguration.statuses[filterID];
581           if (!(filterStatusAllowsFeature(filterStatus, feature))) {
582             return false;
583           }
584         }
585       }
586
587       return true;
588     }
589   };
590
591   /**
592    * Constructor for an editor feature HTML rule.
593    *
594    * Intended to be used in combination with {@link Drupal.EditorFeature}.
595    *
596    * A text editor feature rule object describes both:
597    *  - required HTML tags, attributes, styles and classes: without these, the
598    *    text editor feature is unable to function. It's possible that a
599    *  - allowed HTML tags, attributes, styles and classes: these are optional
600    *    in the strictest sense, but it is possible that the feature generates
601    *    them.
602    *
603    * The structure can be very clearly seen below: there's a "required" and an
604    * "allowed" key. For each of those, there are objects with the "tags",
605    * "attributes", "styles" and "classes" keys. For all these keys the values
606    * are initialized to the empty array. List each possible value as an array
607    * value. Besides the "required" and "allowed" keys, there's an optional
608    * "raw" key: it allows text editor implementations to optionally pass in
609    * their raw representation instead of the Drupal-defined representation for
610    * HTML rules.
611    *
612    * @example
613    * tags: ['<a>']
614    * attributes: ['href', 'alt']
615    * styles: ['color', 'text-decoration']
616    * classes: ['external', 'internal']
617    *
618    * @constructor
619    *
620    * @see Drupal.EditorFeature
621    */
622   Drupal.EditorFeatureHTMLRule = function () {
623
624     /**
625      *
626      * @type {object}
627      *
628      * @prop {Array} tags
629      * @prop {Array} attributes
630      * @prop {Array} styles
631      * @prop {Array} classes
632      */
633     this.required = {tags: [], attributes: [], styles: [], classes: []};
634
635     /**
636      *
637      * @type {object}
638      *
639      * @prop {Array} tags
640      * @prop {Array} attributes
641      * @prop {Array} styles
642      * @prop {Array} classes
643      */
644     this.allowed = {tags: [], attributes: [], styles: [], classes: []};
645
646     /**
647      *
648      * @type {null}
649      */
650     this.raw = null;
651   };
652
653   /**
654    * A text editor feature object. Initialized with the feature name.
655    *
656    * Contains a set of HTML rules ({@link Drupal.EditorFeatureHTMLRule} objects)
657    * that describe which HTML tags, attributes, styles and classes are required
658    * (i.e. essential for the feature to function at all) and which are allowed
659    * (i.e. the feature may generate this, but they're not essential).
660    *
661    * It is necessary to allow for multiple HTML rules per feature: with just
662    * one HTML rule per feature, there is not enough expressiveness to describe
663    * certain cases. For example: a "table" feature would probably require the
664    * `<table>` tag, and might allow e.g. the "summary" attribute on that tag.
665    * However, the table feature would also require the `<tr>` and `<td>` tags,
666    * but it doesn't make sense to allow for a "summary" attribute on these tags.
667    * Hence these would need to be split in two separate rules.
668    *
669    * HTML rules must be added with the `addHTMLRule()` method. A feature that
670    * has zero HTML rules does not create or modify HTML.
671    *
672    * @constructor
673    *
674    * @param {string} name
675    *   The name of the feature.
676    *
677    * @see Drupal.EditorFeatureHTMLRule
678    */
679   Drupal.EditorFeature = function (name) {
680     this.name = name;
681     this.rules = [];
682   };
683
684   /**
685    * Adds a HTML rule to the list of HTML rules for this feature.
686    *
687    * @param {Drupal.EditorFeatureHTMLRule} rule
688    *   A text editor feature HTML rule.
689    */
690   Drupal.EditorFeature.prototype.addHTMLRule = function (rule) {
691     this.rules.push(rule);
692   };
693
694   /**
695    * Text filter status object. Initialized with the filter ID.
696    *
697    * Indicates whether the text filter is currently active (enabled) or not.
698    *
699    * Contains a set of HTML rules ({@link Drupal.FilterHTMLRule} objects) that
700    * describe which HTML tags are allowed or forbidden. They can also describe
701    * for a set of tags (or all tags) which attributes, styles and classes are
702    * allowed and which are forbidden.
703    *
704    * It is necessary to allow for multiple HTML rules per feature, for
705    * analogous reasons as {@link Drupal.EditorFeature}.
706    *
707    * HTML rules must be added with the `addHTMLRule()` method. A filter that has
708    * zero HTML rules does not disallow any HTML.
709    *
710    * @constructor
711    *
712    * @param {string} name
713    *   The name of the feature.
714    *
715    * @see Drupal.FilterHTMLRule
716    */
717   Drupal.FilterStatus = function (name) {
718
719     /**
720      *
721      * @type {string}
722      */
723     this.name = name;
724
725     /**
726      *
727      * @type {bool}
728      */
729     this.active = false;
730
731     /**
732      *
733      * @type {Array.<Drupal.FilterHTMLRule>}
734      */
735     this.rules = [];
736   };
737
738   /**
739    * Adds a HTML rule to the list of HTML rules for this filter.
740    *
741    * @param {Drupal.FilterHTMLRule} rule
742    *   A text filter HTML rule.
743    */
744   Drupal.FilterStatus.prototype.addHTMLRule = function (rule) {
745     this.rules.push(rule);
746   };
747
748   /**
749    * A text filter HTML rule object.
750    *
751    * Intended to be used in combination with {@link Drupal.FilterStatus}.
752    *
753    * A text filter rule object describes:
754    *  1. allowed or forbidden tags: (optional) whitelist or blacklist HTML tags
755    *  2. restricted tag properties: (optional) whitelist or blacklist
756    *     attributes, styles and classes on a set of HTML tags.
757    *
758    * Typically, each text filter rule object does either 1 or 2, not both.
759    *
760    * The structure can be very clearly seen below:
761    *  1. use the "tags" key to list HTML tags, and set the "allow" key to
762    *     either true (to allow these HTML tags) or false (to forbid these HTML
763    *     tags). If you leave the "tags" key's default value (the empty array),
764    *     no restrictions are applied.
765    *  2. all nested within the "restrictedTags" key: use the "tags" subkey to
766    *     list HTML tags to which you want to apply property restrictions, then
767    *     use the "allowed" subkey to whitelist specific property values, and
768    *     similarly use the "forbidden" subkey to blacklist specific property
769    *     values.
770    *
771    * @example
772    * <caption>Whitelist the "p", "strong" and "a" HTML tags.</caption>
773    * {
774    *   tags: ['p', 'strong', 'a'],
775    *   allow: true,
776    *   restrictedTags: {
777    *     tags: [],
778    *     allowed: { attributes: [], styles: [], classes: [] },
779    *     forbidden: { attributes: [], styles: [], classes: [] }
780    *   }
781    * }
782    * @example
783    * <caption>For the "a" HTML tag, only allow the "href" attribute
784    * and the "external" class and disallow the "target" attribute.</caption>
785    * {
786    *   tags: [],
787    *   allow: null,
788    *   restrictedTags: {
789    *     tags: ['a'],
790    *     allowed: { attributes: ['href'], styles: [], classes: ['external'] },
791    *     forbidden: { attributes: ['target'], styles: [], classes: [] }
792    *   }
793    * }
794    * @example
795    * <caption>For all tags, allow the "data-*" attribute (that is, any
796    * attribute that begins with "data-").</caption>
797    * {
798    *   tags: [],
799    *   allow: null,
800    *   restrictedTags: {
801    *     tags: ['*'],
802    *     allowed: { attributes: ['data-*'], styles: [], classes: [] },
803    *     forbidden: { attributes: [], styles: [], classes: [] }
804    *   }
805    * }
806    *
807    * @return {object}
808    *   An object with the following structure:
809    * ```
810    * {
811    *   tags: Array,
812    *   allow: null,
813    *   restrictedTags: {
814    *     tags: Array,
815    *     allowed: {attributes: Array, styles: Array, classes: Array},
816    *     forbidden: {attributes: Array, styles: Array, classes: Array}
817    *   }
818    * }
819    * ```
820    *
821    * @see Drupal.FilterStatus
822    */
823   Drupal.FilterHTMLRule = function () {
824     // Allow or forbid tags.
825     this.tags = [];
826     this.allow = null;
827
828     // Apply restrictions to properties set on tags.
829     this.restrictedTags = {
830       tags: [],
831       allowed: {attributes: [], styles: [], classes: []},
832       forbidden: {attributes: [], styles: [], classes: []}
833     };
834
835     return this;
836   };
837
838   Drupal.FilterHTMLRule.prototype.clone = function () {
839     var clone = new Drupal.FilterHTMLRule();
840     clone.tags = this.tags.slice(0);
841     clone.allow = this.allow;
842     clone.restrictedTags.tags = this.restrictedTags.tags.slice(0);
843     clone.restrictedTags.allowed.attributes = this.restrictedTags.allowed.attributes.slice(0);
844     clone.restrictedTags.allowed.styles = this.restrictedTags.allowed.styles.slice(0);
845     clone.restrictedTags.allowed.classes = this.restrictedTags.allowed.classes.slice(0);
846     clone.restrictedTags.forbidden.attributes = this.restrictedTags.forbidden.attributes.slice(0);
847     clone.restrictedTags.forbidden.styles = this.restrictedTags.forbidden.styles.slice(0);
848     clone.restrictedTags.forbidden.classes = this.restrictedTags.forbidden.classes.slice(0);
849     return clone;
850   };
851
852   /**
853    * Tracks the configuration of all text filters in {@link Drupal.FilterStatus}
854    * objects for {@link Drupal.editorConfiguration.featureIsAllowedByFilters}.
855    *
856    * @namespace
857    */
858   Drupal.filterConfiguration = {
859
860     /**
861      * Drupal.FilterStatus objects, keyed by filter ID.
862      *
863      * @type {Object.<string, Drupal.FilterStatus>}
864      */
865     statuses: {},
866
867     /**
868      * Live filter setting parsers.
869      *
870      * Object keyed by filter ID, for those filters that implement it.
871      *
872      * Filters should load the implementing JavaScript on the filter
873      * configuration form and implement
874      * `Drupal.filterSettings[filterID].getRules()`, which should return an
875      * array of {@link Drupal.FilterHTMLRule} objects.
876      *
877      * @namespace
878      */
879     liveSettingParsers: {},
880
881     /**
882      * Updates all {@link Drupal.FilterStatus} objects to reflect current state.
883      *
884      * Automatically checks whether a filter is currently enabled or not. To
885      * support more finegrained.
886      *
887      * If a filter implements a live setting parser, then that will be used to
888      * keep the HTML rules for the {@link Drupal.FilterStatus} object
889      * up-to-date.
890      */
891     update: function () {
892       for (var filterID in Drupal.filterConfiguration.statuses) {
893         if (Drupal.filterConfiguration.statuses.hasOwnProperty(filterID)) {
894           // Update status.
895           Drupal.filterConfiguration.statuses[filterID].active = $('[name="filters[' + filterID + '][status]"]').is(':checked');
896
897           // Update current rules.
898           if (Drupal.filterConfiguration.liveSettingParsers[filterID]) {
899             Drupal.filterConfiguration.statuses[filterID].rules = Drupal.filterConfiguration.liveSettingParsers[filterID].getRules();
900           }
901         }
902       }
903     }
904
905   };
906
907   /**
908    * Initializes {@link Drupal.filterConfiguration}.
909    *
910    * @type {Drupal~behavior}
911    *
912    * @prop {Drupal~behaviorAttach} attach
913    *   Gets filter configuration from filter form input.
914    */
915   Drupal.behaviors.initializeFilterConfiguration = {
916     attach: function (context, settings) {
917       var $context = $(context);
918
919       $context.find('#filters-status-wrapper input.form-checkbox').once('filter-editor-status').each(function () {
920         var $checkbox = $(this);
921         var nameAttribute = $checkbox.attr('name');
922
923         // The filter's checkbox has a name attribute of the form
924         // "filters[<name of filter>][status]", parse "<name of filter>"
925         // from it.
926         var filterID = nameAttribute.substring(8, nameAttribute.indexOf(']'));
927
928         // Create a Drupal.FilterStatus object to track the state (whether it's
929         // active or not and its current settings, if any) of each filter.
930         Drupal.filterConfiguration.statuses[filterID] = new Drupal.FilterStatus(filterID);
931       });
932     }
933   };
934
935 })(jQuery, _, Drupal, document);