text: /**
   * @file
   * Attaches behavior for updating filter_html's settings automatically.
   */
  
  (function ($, Drupal, _, document) {
    if (Drupal.filterConfiguration) {
      /**
       * Implement a live setting parser to prevent text editors from automatically
       * enabling buttons that are not allowed by this filter's configuration.
       *
       * @namespace
       */
      Drupal.filterConfiguration.liveSettingParsers.filter_html = {
        /**
         * @return {Array}
         *   An array of filter rules.
         */
        getRules() {
          const currentValue = $(
            '#edit-filters-filter-html-settings-allowed-html',
          ).val();
          const rules =
            Drupal.behaviors.filterFilterHtmlUpdating._parseSetting(currentValue);
  
          // Build a FilterHTMLRule that reflects the hard-coded behavior that
          // strips all "style" attribute and all "on*" attributes.
          const rule = new Drupal.FilterHTMLRule();
          rule.restrictedTags.tags = ['*'];
          rule.restrictedTags.forbidden.attributes = ['style', 'on*'];
          rules.push(rule);
  
          return rules;
        },
      };
    }
  
    /**
     * Displays and updates what HTML tags are allowed to use in a filter.
     *
     * @type {Drupal~behavior}
     *
     * @todo Remove everything but 'attach' and 'detach' and make a proper object.
     *
     * @prop {Drupal~behaviorAttach} attach
     *   Attaches behavior for updating allowed HTML tags.
     */
    Drupal.behaviors.filterFilterHtmlUpdating = {
      // The form item contains the "Allowed HTML tags" setting.
      $allowedHTMLFormItem: null,
  
      // The description for the "Allowed HTML tags" field.
      $allowedHTMLDescription: null,
  
      /**
       * The parsed, user-entered tag list of $allowedHTMLFormItem
       *
       * @var {Object.<string, Drupal.FilterHTMLRule>}
       */
      userTags: {},
  
      // The auto-created tag list thus far added.
      autoTags: null,
  
      // Track which new features have been added to the text editor.
      newFeatures: {},
  
      attach(context, settings) {
        const that = this;
        $(context)
          .find('[name="filters[filter_html][settings][allowed_html]"]')
          .once('filter-filter_html-updating')
          .each(function () {
            that.$allowedHTMLFormItem = $(this);
            that.$allowedHTMLDescription = that.$allowedHTMLFormItem
              .closest('.js-form-item')
              .find('.description');
            that.userTags = that._parseSetting(this.value);
  
            // Update the new allowed tags based on added text editor features.
            $(document)
              .on('drupalEditorFeatureAdded', (e, feature) => {
                that.newFeatures[feature.name] = feature.rules;
                that._updateAllowedTags();
              })
              .on('drupalEditorFeatureModified', (e, feature) => {
                if (that.newFeatures.hasOwnProperty(feature.name)) {
                  that.newFeatures[feature.name] = feature.rules;
                  that._updateAllowedTags();
                }
              })
              .on('drupalEditorFeatureRemoved', (e, feature) => {
                if (that.newFeatures.hasOwnProperty(feature.name)) {
                  delete that.newFeatures[feature.name];
                  that._updateAllowedTags();
                }
              });
  
            // When the allowed tags list is manually changed, update userTags.
            that.$allowedHTMLFormItem.on('change.updateUserTags', function () {
              that.userTags = _.difference(
                that._parseSetting(this.value),
                that.autoTags,
              );
            });
          });
      },
  
      /**
       * Updates the "Allowed HTML tags" setting and shows an informative message.
       */
      _updateAllowedTags() {
        // Update the list of auto-created tags.
        this.autoTags = this._calculateAutoAllowedTags(
          this.userTags,
          this.newFeatures,
        );
  
        // Remove any previous auto-created tag message.
        this.$allowedHTMLDescription.find('.editor-update-message').remove();
  
        // If any auto-created tags: insert message and update form item.
        if (!_.isEmpty(this.autoTags)) {
          this.$allowedHTMLDescription.append(
            Drupal.theme('filterFilterHTMLUpdateMessage', this.autoTags),
          );
          const userTagsWithoutOverrides = _.omit(
            this.userTags,
            _.keys(this.autoTags),
          );
          this.$allowedHTMLFormItem.val(
            `${this._generateSetting(
              userTagsWithoutOverrides,
            )} ${this._generateSetting(this.autoTags)}`,
          );
        }
        // Restore to original state.
        else {
          this.$allowedHTMLFormItem.val(this._generateSetting(this.userTags));
        }
      },
  
      /**
       * Calculates which HTML tags the added text editor buttons need to work.
       *
       * The filter_html filter is only concerned with the required tags, not with
       * any properties, nor with each feature's "allowed" tags.
       *
       * @param {Array} userAllowedTags
       *   The list of user-defined allowed tags.
       * @param {object} newFeatures
       *   A list of {@link Drupal.EditorFeature} objects' rules, keyed by
       *   their name.
       *
       * @return {Array}
       *   A list of new allowed tags.
       */
      _calculateAutoAllowedTags(userAllowedTags, newFeatures) {
        const editorRequiredTags = {};
  
        // Map the newly added Text Editor features to Drupal.FilterHtmlRule
        // objects (to allow comparing userTags with autoTags).
        Object.keys(newFeatures || {}).forEach((featureName) => {
          const feature = newFeatures[featureName];
          let featureRule;
          let filterRule;
          let tag;
  
          for (let f = 0; f < feature.length; f++) {
            featureRule = feature[f];
            for (let t = 0; t < featureRule.required.tags.length; t++) {
              tag = featureRule.required.tags[t];
              if (!_.has(editorRequiredTags, tag)) {
                filterRule = new Drupal.FilterHTMLRule();
                filterRule.restrictedTags.tags = [tag];
                // @todo Neither Drupal.FilterHtmlRule nor
                //   Drupal.EditorFeatureHTMLRule allow for generic attribute
                //   value restrictions, only for the "class" and "style"
                //   attribute's values to be restricted. The filter_html filter
                //   always disallows the "style" attribute, so we only need to
                //   support "class" attribute value restrictions. Fix once
                //   https://www.drupal.org/node/2567801 lands.
                filterRule.restrictedTags.allowed.attributes =
                  featureRule.required.attributes.slice(0);
                filterRule.restrictedTags.allowed.classes =
                  featureRule.required.classes.slice(0);
                editorRequiredTags[tag] = filterRule;
              }
              // The tag is already allowed, add any additionally allowed
              // attributes.
              else {
                filterRule = editorRequiredTags[tag];
                filterRule.restrictedTags.allowed.attributes = _.union(
                  filterRule.restrictedTags.allowed.attributes,
                  featureRule.required.attributes,
                );
                filterRule.restrictedTags.allowed.classes = _.union(
                  filterRule.restrictedTags.allowed.classes,
                  featureRule.required.classes,
                );
              }
            }
          }
        });
  
        // Now compare userAllowedTags with editorRequiredTags, and build
        // autoAllowedTags, which contains:
        // - any tags in editorRequiredTags but not in userAllowedTags (i.e. tags
        //   that are additionally going to be allowed)
        // - any tags in editorRequiredTags that already exists in userAllowedTags
        //   but does not allow all attributes or attribute values
        const autoAllowedTags = {};
        Object.keys(editorRequiredTags).forEach((tag) => {
          // If userAllowedTags does not contain a rule for this editor-required
          // tag, then add it to the list of automatically allowed tags.
          if (!_.has(userAllowedTags, tag)) {
            autoAllowedTags[tag] = editorRequiredTags[tag];
          }
          // Otherwise, if userAllowedTags already allows this tag, then check if
          // additional attributes and classes on this tag are required by the
          // editor.
          else {
            const requiredAttributes =
              editorRequiredTags[tag].restrictedTags.allowed.attributes;
            const allowedAttributes =
              userAllowedTags[tag].restrictedTags.allowed.attributes;
            const needsAdditionalAttributes =
              requiredAttributes.length &&
              _.difference(requiredAttributes, allowedAttributes).length;
            const requiredClasses =
              editorRequiredTags[tag].restrictedTags.allowed.classes;
            const allowedClasses =
              userAllowedTags[tag].restrictedTags.allowed.classes;
            const needsAdditionalClasses =
              requiredClasses.length &&
              _.difference(requiredClasses, allowedClasses).length;
            if (needsAdditionalAttributes || needsAdditionalClasses) {
              autoAllowedTags[tag] = userAllowedTags[tag].clone();
            }
            if (needsAdditionalAttributes) {
              autoAllowedTags[tag].restrictedTags.allowed.attributes = _.union(
                allowedAttributes,
                requiredAttributes,
              );
            }
            if (needsAdditionalClasses) {
              autoAllowedTags[tag].restrictedTags.allowed.classes = _.union(
                allowedClasses,
                requiredClasses,
              );
            }
          }
        });
  
        return autoAllowedTags;
      },
  
      /**
       * Parses the value of this.$allowedHTMLFormItem.
       *
       * @param {string} setting
       *   The string representation of the setting. For example:
       *     <p class="callout"> <br> <a href hreflang>
       *
       * @return {Object.<string, Drupal.FilterHTMLRule>}
       *   The corresponding text filter HTML rule objects, one per tag, keyed by
       *   tag name.
       */
      _parseSetting(setting) {
        let node;
        let tag;
        let rule;
        let attributes;
        let attribute;
        const allowedTags = setting.match(/(<[^>]+>)/g);
        const sandbox = document.createElement('div');
        const rules = {};
        for (let t = 0; t < allowedTags.length; t++) {
          // Let the browser do the parsing work for us.
          sandbox.innerHTML = allowedTags[t];
          node = sandbox.firstChild;
          tag = node.tagName.toLowerCase();
  
          // Build the Drupal.FilterHtmlRule object.
          rule = new Drupal.FilterHTMLRule();
          // We create one rule per allowed tag, so always one tag.
          rule.restrictedTags.tags = [tag];
          // Add the attribute restrictions.
          attributes = node.attributes;
          for (let i = 0; i < attributes.length; i++) {
            attribute = attributes.item(i);
            const attributeName = attribute.nodeName;
            // @todo Drupal.FilterHtmlRule does not allow for generic attribute
            //   value restrictions, only for the "class" and "style" attribute's
            //   values. The filter_html filter always disallows the "style"
            //   attribute, so we only need to support "class" attribute value
            //   restrictions. Fix once https://www.drupal.org/node/2567801 lands.
            if (attributeName === 'class') {
              const attributeValue = attribute.textContent;
              rule.restrictedTags.allowed.classes = attributeValue.split(' ');
            } else {
              rule.restrictedTags.allowed.attributes.push(attributeName);
            }
          }
  
          rules[tag] = rule;
        }
        return rules;
      },
  
      /**
       * Generates the value of this.$allowedHTMLFormItem.
       *
       * @param {Object.<string, Drupal.FilterHTMLRule>} tags
       *   The parsed representation of the setting.
       *
       * @return {Array}
       *   The string representation of the setting. e.g. "<p> <br> <a>"
       */
      _generateSetting(tags) {
        return _.reduce(
          tags,
          (setting, rule, tag) => {
            if (setting.length) {
              setting += ' ';
            }
  
            setting += `<${tag}`;
            if (rule.restrictedTags.allowed.attributes.length) {
              setting += ` ${rule.restrictedTags.allowed.attributes.join(' ')}`;
            }
            // @todo Drupal.FilterHtmlRule does not allow for generic attribute
            //   value restrictions, only for the "class" and "style" attribute's
            //   values. The filter_html filter always disallows the "style"
            //   attribute, so we only need to support "class" attribute value
            //   restrictions. Fix once https://www.drupal.org/node/2567801 lands.
            if (rule.restrictedTags.allowed.classes.length) {
              setting += ` class="${rule.restrictedTags.allowed.classes.join(
                ' ',
              )}"`;
            }
  
            setting += '>';
            return setting;
          },
          '',
        );
      },
    };
  
    /**
     * Theme function for the filter_html update message.
     *
     * @param {Array} tags
     *   An array of the new tags that are to be allowed.
     *
     * @return {string}
     *   The corresponding HTML.
     */
    Drupal.theme.filterFilterHTMLUpdateMessage = function (tags) {
      let html = '';
      const tagList =
        Drupal.behaviors.filterFilterHtmlUpdating._generateSetting(tags);
      html += '<p class="editor-update-message">';
      html += Drupal.t(
        'Based on the text editor configuration, these tags have automatically been added: <strong>@tag-list</strong>.',
        { '@tag-list': tagList },
      );
      html += '</p>';
      return html;
    };
  })(jQuery, Drupal, _, document);
  