/** * @file * Provides a JavaScript API to broadcast text editor configuration changes. * * Filter implementations may listen to the drupalEditorFeatureAdded, * drupalEditorFeatureRemoved, and drupalEditorFeatureRemoved events on document * to automatically adjust their settings based on the editor configuration. */ (function ($, _, Drupal, document) { /** * Editor configuration namespace. * * @namespace */ Drupal.editorConfiguration = { /** * Must be called by a specific text editor's configuration whenever a * feature is added by the user. * * Triggers the drupalEditorFeatureAdded event on the document, which * receives a {@link Drupal.EditorFeature} object. * * @param {Drupal.EditorFeature} feature * A text editor feature object. * * @fires event:drupalEditorFeatureAdded */ addedFeature(feature) { $(document).trigger('drupalEditorFeatureAdded', feature); }, /** * Must be called by a specific text editor's configuration whenever a * feature is removed by the user. * * Triggers the drupalEditorFeatureRemoved event on the document, which * receives a {@link Drupal.EditorFeature} object. * * @param {Drupal.EditorFeature} feature * A text editor feature object. * * @fires event:drupalEditorFeatureRemoved */ removedFeature(feature) { $(document).trigger('drupalEditorFeatureRemoved', feature); }, /** * Must be called by a specific text editor's configuration whenever a * feature is modified, i.e. has different rules. * * For example when the "Bold" button is configured to use the `` tag * instead of the `` tag. * * Triggers the drupalEditorFeatureModified event on the document, which * receives a {@link Drupal.EditorFeature} object. * * @param {Drupal.EditorFeature} feature * A text editor feature object. * * @fires event:drupalEditorFeatureModified */ modifiedFeature(feature) { $(document).trigger('drupalEditorFeatureModified', feature); }, /** * May be called by a specific text editor's configuration whenever a * feature is being added, to check whether it would require the filter * settings to be updated. * * The canonical use case is when a text editor is being enabled: * preferably * this would not cause the filter settings to be changed; rather, the * default set of buttons (features) for the text editor should adjust * itself to not cause filter setting changes. * * Note: for filters to integrate with this functionality, it is necessary * that they implement * `Drupal.filterSettingsForEditors[filterID].getRules()`. * * @param {Drupal.EditorFeature} feature * A text editor feature object. * * @return {bool} * Whether the given feature is allowed by the current filters. */ featureIsAllowedByFilters(feature) { /** * Generate the universe U of possible values that can result from the * feature's rules' requirements. * * This generates an object of this form: * var universe = { * a: { * 'touchedByAllowedPropertyRule': false, * 'tag': false, * 'attributes:href': false, * 'classes:external': false, * }, * strong: { * 'touchedByAllowedPropertyRule': false, * 'tag': false, * }, * img: { * 'touchedByAllowedPropertyRule': false, * 'tag': false, * 'attributes:src': false * } * }; * * In this example, the given text editor feature resulted in the above * universe, which shows that it must be allowed to generate the a, * strong and img tags. For the a tag, it must be able to set the "href" * attribute and the "external" class. For the strong tag, no further * properties are required. For the img tag, the "src" attribute is * required. The "tag" key is used to track whether that tag was * explicitly allowed by one of the filter's rules. The * "touchedByAllowedPropertyRule" key is used for state tracking that is * essential for filterStatusAllowsFeature() to be able to reason: when * all of a filter's rules have been applied, and none of the forbidden * rules matched (which would have resulted in early termination) yet the * universe has not been made empty (which would be the end result if * everything in the universe were explicitly allowed), then this piece * of state data enables us to determine whether a tag whose properties * were not all explicitly allowed are in fact still allowed, because its * tag was explicitly allowed and there were no filter rules applying * "allowed tag property value" restrictions for this particular tag. * * @param {object} feature * The feature in question. * * @return {object} * The universe generated. * * @see findPropertyValueOnTag() * @see filterStatusAllowsFeature() */ function generateUniverseFromFeatureRequirements(feature) { const properties = ['attributes', 'styles', 'classes']; const universe = {}; for (let r = 0; r < feature.rules.length; r++) { const featureRule = feature.rules[r]; // For each tag required by this feature rule, create a basic entry in // the universe. const requiredTags = featureRule.required.tags; for (let t = 0; t < requiredTags.length; t++) { universe[requiredTags[t]] = { // Whether this tag was allowed or not. tag: false, // Whether any filter rule that applies to this tag had an allowed // property rule. i.e. will become true if >=1 filter rule has >=1 // allowed property rule. touchedByAllowedPropertyRule: false, // Analogous, but for forbidden property rule. touchedBytouchedByForbiddenPropertyRule: false, }; } // If no required properties are defined for this rule, we can move on // to the next feature. if (emptyProperties(featureRule.required)) { continue; } // Expand the existing universe, assume that each tags' property // value is disallowed. If the filter rules allow everything in the // feature's universe, then the feature is allowed. for (let p = 0; p < properties.length; p++) { const property = properties[p]; for (let pv = 0; pv < featureRule.required[property].length; pv++) { const propertyValue = featureRule.required[property]; universe[requiredTags][`${property}:${propertyValue}`] = false; } } } return universe; } /** * Provided a section of a feature or filter rule, checks if no property * values are defined for all properties: attributes, classes and styles. * * @param {object} section * The section to check. * * @return {bool} * Returns true if the section has empty properties, false otherwise. */ function emptyProperties(section) { return section.attributes.length === 0 && section.classes.length === 0 && section.styles.length === 0; } /** * Calls findPropertyValueOnTag on the given tag for every property value * that is listed in the "propertyValues" parameter. Supports the wildcard * tag. * * @param {object} universe * The universe to check. * @param {string} tag * The tag to look for. * @param {string} property * The property to check. * @param {Array} propertyValues * Values of the property to check. * @param {bool} allowing * Whether to update the universe or not. * * @return {bool} * Returns true if found, false otherwise. */ function findPropertyValuesOnTag(universe, tag, property, propertyValues, allowing) { // Detect the wildcard case. if (tag === '*') { return findPropertyValuesOnAllTags(universe, property, propertyValues, allowing); } let atLeastOneFound = false; _.each(propertyValues, (propertyValue) => { if (findPropertyValueOnTag(universe, tag, property, propertyValue, allowing)) { atLeastOneFound = true; } }); return atLeastOneFound; } /** * Calls findPropertyValuesOnAllTags for all tags in the universe. * * @param {object} universe * The universe to check. * @param {string} property * The property to check. * @param {Array} propertyValues * Values of the property to check. * @param {bool} allowing * Whether to update the universe or not. * * @return {bool} * Returns true if found, false otherwise. */ function findPropertyValuesOnAllTags(universe, property, propertyValues, allowing) { let atLeastOneFound = false; _.each(_.keys(universe), (tag) => { if (findPropertyValuesOnTag(universe, tag, property, propertyValues, allowing)) { atLeastOneFound = true; } }); return atLeastOneFound; } /** * Finds out if a specific property value (potentially containing * wildcards) exists on the given tag. When the "allowing" parameter * equals true, the universe will be updated if that specific property * value exists. Returns true if found, false otherwise. * * @param {object} universe * The universe to check. * @param {string} tag * The tag to look for. * @param {string} property * The property to check. * @param {string} propertyValue * The property value to check. * @param {bool} allowing * Whether to update the universe or not. * * @return {bool} * Returns true if found, false otherwise. */ function findPropertyValueOnTag(universe, tag, property, propertyValue, allowing) { // If the tag does not exist in the universe, then it definitely can't // have this specific property value. if (!_.has(universe, tag)) { return false; } const key = `${property}:${propertyValue}`; // Track whether a tag was touched by a filter rule that allows specific // property values on this particular tag. // @see generateUniverseFromFeatureRequirements if (allowing) { universe[tag].touchedByAllowedPropertyRule = true; } // The simple case: no wildcard in property value. if (_.indexOf(propertyValue, '*') === -1) { if (_.has(universe, tag) && _.has(universe[tag], key)) { if (allowing) { universe[tag][key] = true; } return true; } return false; } // The complex case: wildcard in property value. let atLeastOneFound = false; const regex = key.replace(/\*/g, '[^ ]*'); _.each(_.keys(universe[tag]), (key) => { if (key.match(regex)) { atLeastOneFound = true; if (allowing) { universe[tag][key] = true; } } }); return atLeastOneFound; } /** * Deletes a tag from the universe if the tag itself and each of its * properties are marked as allowed. * * @param {object} universe * The universe to delete from. * @param {string} tag * The tag to check. * * @return {bool} * Whether something was deleted from the universe. */ function deleteFromUniverseIfAllowed(universe, tag) { // Detect the wildcard case. if (tag === '*') { return deleteAllTagsFromUniverseIfAllowed(universe); } if (_.has(universe, tag) && _.every(_.omit(universe[tag], 'touchedByAllowedPropertyRule'))) { delete universe[tag]; return true; } return false; } /** * Calls deleteFromUniverseIfAllowed for all tags in the universe. * * @param {object} universe * The universe to delete from. * * @return {bool} * Whether something was deleted from the universe. */ function deleteAllTagsFromUniverseIfAllowed(universe) { let atLeastOneDeleted = false; _.each(_.keys(universe), (tag) => { if (deleteFromUniverseIfAllowed(universe, tag)) { atLeastOneDeleted = true; } }); return atLeastOneDeleted; } /** * Checks if any filter rule forbids either a tag or a tag property value * that exists in the universe. * * @param {object} universe * Universe to check. * @param {object} filterStatus * Filter status to use for check. * * @return {bool} * Whether any filter rule forbids something in the universe. */ function anyForbiddenFilterRuleMatches(universe, filterStatus) { const properties = ['attributes', 'styles', 'classes']; // Check if a tag in the universe is forbidden. const allRequiredTags = _.keys(universe); let filterRule; for (let i = 0; i < filterStatus.rules.length; i++) { filterRule = filterStatus.rules[i]; if (filterRule.allow === false) { if (_.intersection(allRequiredTags, filterRule.tags).length > 0) { return true; } } } // Check if a property value of a tag in the universe is forbidden. // For all filter rules… for (let n = 0; n < filterStatus.rules.length; n++) { filterRule = filterStatus.rules[n]; // … if there are tags with restricted property values … if (filterRule.restrictedTags.tags.length && !emptyProperties(filterRule.restrictedTags.forbidden)) { // … for all those tags … for (let j = 0; j < filterRule.restrictedTags.tags.length; j++) { const tag = filterRule.restrictedTags.tags[j]; // … then iterate over all properties … for (let k = 0; k < properties.length; k++) { const property = properties[k]; // … and return true if just one of the forbidden property // values for this tag and property is listed in the universe. if (findPropertyValuesOnTag(universe, tag, property, filterRule.restrictedTags.forbidden[property], false)) { return true; } } } } } return false; } /** * Applies every filter rule's explicit allowing of a tag or a tag * property value to the universe. Whenever both the tag and all of its * required property values are marked as explicitly allowed, they are * deleted from the universe. * * @param {object} universe * Universe to delete from. * @param {object} filterStatus * The filter status in question. */ function markAllowedTagsAndPropertyValues(universe, filterStatus) { const properties = ['attributes', 'styles', 'classes']; // Check if a tag in the universe is allowed. let filterRule; let tag; for (let l = 0; !_.isEmpty(universe) && l < filterStatus.rules.length; l++) { filterRule = filterStatus.rules[l]; if (filterRule.allow === true) { for (let m = 0; !_.isEmpty(universe) && m < filterRule.tags.length; m++) { tag = filterRule.tags[m]; if (_.has(universe, tag)) { universe[tag].tag = true; deleteFromUniverseIfAllowed(universe, tag); } } } } // Check if a property value of a tag in the universe is allowed. // For all filter rules… for (let i = 0; !_.isEmpty(universe) && i < filterStatus.rules.length; i++) { filterRule = filterStatus.rules[i]; // … if there are tags with restricted property values … if (filterRule.restrictedTags.tags.length && !emptyProperties(filterRule.restrictedTags.allowed)) { // … for all those tags … for (let j = 0; !_.isEmpty(universe) && j < filterRule.restrictedTags.tags.length; j++) { tag = filterRule.restrictedTags.tags[j]; // … then iterate over all properties … for (let k = 0; k < properties.length; k++) { const property = properties[k]; // … and try to delete this tag from the universe if just one // of the allowed property values for this tag and property is // listed in the universe. (Because everything might be allowed // now.) if (findPropertyValuesOnTag(universe, tag, property, filterRule.restrictedTags.allowed[property], true)) { deleteFromUniverseIfAllowed(universe, tag); } } } } } } /** * Checks whether the current status of a filter allows a specific feature * by building the universe of potential values from the feature's * requirements and then checking whether anything in the filter prevents * that. * * @param {object} filterStatus * The filter status in question. * @param {object} feature * The feature requested. * * @return {bool} * Whether the current status of the filter allows specified feature. * * @see generateUniverseFromFeatureRequirements() */ function filterStatusAllowsFeature(filterStatus, feature) { // An inactive filter by definition allows the feature. if (!filterStatus.active) { return true; } // A feature that specifies no rules has no HTML requirements and is // hence allowed by definition. if (feature.rules.length === 0) { return true; } // Analogously for a filter that specifies no rules. if (filterStatus.rules.length === 0) { return true; } // Generate the universe U of possible values that can result from the // feature's rules' requirements. const universe = generateUniverseFromFeatureRequirements(feature); // If anything that is in the universe (and is thus required by the // feature) is forbidden by any of the filter's rules, then this filter // does not allow this feature. if (anyForbiddenFilterRuleMatches(universe, filterStatus)) { return false; } // Mark anything in the universe that is allowed by any of the filter's // rules as allowed. If everything is explicitly allowed, then the // universe will become empty. markAllowedTagsAndPropertyValues(universe, filterStatus); // If there was at least one filter rule allowing tags, then everything // in the universe must be allowed for this feature to be allowed, and // thus by now it must be empty. However, it is still possible that the // filter allows the feature, due to no rules for allowing tag property // values and/or rules for forbidding tag property values. For details: // see the comments below. // @see generateUniverseFromFeatureRequirements() if (_.some(_.pluck(filterStatus.rules, 'allow'))) { // If the universe is empty, then everything was explicitly allowed // and our job is done: this filter allows this feature! if (_.isEmpty(universe)) { return true; } // Otherwise, it is still possible that this feature is allowed. // Every tag must be explicitly allowed if there are filter rules // doing tag whitelisting. if (!_.every(_.pluck(universe, 'tag'))) { return false; } // Every tag was explicitly allowed, but since the universe is not // empty, one or more tag properties are disallowed. However, if // only blacklisting of tag properties was applied to these tags, // and no whitelisting was ever applied, then it's still fine: // since none of the tag properties were blacklisted, we got to // this point, and since no whitelisting was applied, it doesn't // matter that the properties: this could never have happened // anyway. It's only this late that we can know this for certain. const tags = _.keys(universe); // Figure out if there was any rule applying whitelisting tag // restrictions to each of the remaining tags. for (let i = 0; i < tags.length; i++) { const tag = tags[i]; if (_.has(universe, tag)) { if (universe[tag].touchedByAllowedPropertyRule === false) { delete universe[tag]; } } } return _.isEmpty(universe); } // Otherwise, if all filter rules were doing blacklisting, then the sole // fact that we got to this point indicates that this filter allows for // everything that is required for this feature. return true; } // If any filter's current status forbids the editor feature, return // false. Drupal.filterConfiguration.update(); // eslint-disable-next-line no-restricted-syntax for (const filterID in Drupal.filterConfiguration.statuses) { if (Drupal.filterConfiguration.statuses.hasOwnProperty(filterID)) { const filterStatus = Drupal.filterConfiguration.statuses[filterID]; if (!(filterStatusAllowsFeature(filterStatus, feature))) { return false; } } } return true; }, }; /** * Constructor for an editor feature HTML rule. * * Intended to be used in combination with {@link Drupal.EditorFeature}. * * A text editor feature rule object describes both: * - required HTML tags, attributes, styles and classes: without these, the * text editor feature is unable to function. It's possible that a * - allowed HTML tags, attributes, styles and classes: these are optional * in the strictest sense, but it is possible that the feature generates * them. * * The structure can be very clearly seen below: there's a "required" and an * "allowed" key. For each of those, there are objects with the "tags", * "attributes", "styles" and "classes" keys. For all these keys the values * are initialized to the empty array. List each possible value as an array * value. Besides the "required" and "allowed" keys, there's an optional * "raw" key: it allows text editor implementations to optionally pass in * their raw representation instead of the Drupal-defined representation for * HTML rules. * * @example * tags: [''] * attributes: ['href', 'alt'] * styles: ['color', 'text-decoration'] * classes: ['external', 'internal'] * * @constructor * * @see Drupal.EditorFeature */ Drupal.EditorFeatureHTMLRule = function () { /** * * @type {object} * * @prop {Array} tags * @prop {Array} attributes * @prop {Array} styles * @prop {Array} classes */ this.required = { tags: [], attributes: [], styles: [], classes: [] }; /** * * @type {object} * * @prop {Array} tags * @prop {Array} attributes * @prop {Array} styles * @prop {Array} classes */ this.allowed = { tags: [], attributes: [], styles: [], classes: [] }; /** * * @type {null} */ this.raw = null; }; /** * A text editor feature object. Initialized with the feature name. * * Contains a set of HTML rules ({@link Drupal.EditorFeatureHTMLRule} objects) * that describe which HTML tags, attributes, styles and classes are required * (i.e. essential for the feature to function at all) and which are allowed * (i.e. the feature may generate this, but they're not essential). * * It is necessary to allow for multiple HTML rules per feature: with just * one HTML rule per feature, there is not enough expressiveness to describe * certain cases. For example: a "table" feature would probably require the * `` tag, and might allow e.g. the "summary" attribute on that tag. * However, the table feature would also require the `` and ` * { * tags: ['p', 'strong', 'a'], * allow: true, * restrictedTags: { * tags: [], * allowed: { attributes: [], styles: [], classes: [] }, * forbidden: { attributes: [], styles: [], classes: [] } * } * } * @example * * { * tags: [], * allow: null, * restrictedTags: { * tags: ['a'], * allowed: { attributes: ['href'], styles: [], classes: ['external'] }, * forbidden: { attributes: ['target'], styles: [], classes: [] } * } * } * @example * * { * tags: [], * allow: null, * restrictedTags: { * tags: ['*'], * allowed: { attributes: ['data-*'], styles: [], classes: [] }, * forbidden: { attributes: [], styles: [], classes: [] } * } * } * * @return {object} * An object with the following structure: * ``` * { * tags: Array, * allow: null, * restrictedTags: { * tags: Array, * allowed: {attributes: Array, styles: Array, classes: Array}, * forbidden: {attributes: Array, styles: Array, classes: Array} * } * } * ``` * * @see Drupal.FilterStatus */ Drupal.FilterHTMLRule = function () { // Allow or forbid tags. this.tags = []; this.allow = null; // Apply restrictions to properties set on tags. this.restrictedTags = { tags: [], allowed: { attributes: [], styles: [], classes: [] }, forbidden: { attributes: [], styles: [], classes: [] }, }; return this; }; Drupal.FilterHTMLRule.prototype.clone = function () { const clone = new Drupal.FilterHTMLRule(); clone.tags = this.tags.slice(0); clone.allow = this.allow; clone.restrictedTags.tags = this.restrictedTags.tags.slice(0); clone.restrictedTags.allowed.attributes = this.restrictedTags.allowed.attributes.slice(0); clone.restrictedTags.allowed.styles = this.restrictedTags.allowed.styles.slice(0); clone.restrictedTags.allowed.classes = this.restrictedTags.allowed.classes.slice(0); clone.restrictedTags.forbidden.attributes = this.restrictedTags.forbidden.attributes.slice(0); clone.restrictedTags.forbidden.styles = this.restrictedTags.forbidden.styles.slice(0); clone.restrictedTags.forbidden.classes = this.restrictedTags.forbidden.classes.slice(0); return clone; }; /** * Tracks the configuration of all text filters in {@link Drupal.FilterStatus} * objects for {@link Drupal.editorConfiguration.featureIsAllowedByFilters}. * * @namespace */ Drupal.filterConfiguration = { /** * Drupal.FilterStatus objects, keyed by filter ID. * * @type {Object.} */ statuses: {}, /** * Live filter setting parsers. * * Object keyed by filter ID, for those filters that implement it. * * Filters should load the implementing JavaScript on the filter * configuration form and implement * `Drupal.filterSettings[filterID].getRules()`, which should return an * array of {@link Drupal.FilterHTMLRule} objects. * * @namespace */ liveSettingParsers: {}, /** * Updates all {@link Drupal.FilterStatus} objects to reflect current state. * * Automatically checks whether a filter is currently enabled or not. To * support more finegrained. * * If a filter implements a live setting parser, then that will be used to * keep the HTML rules for the {@link Drupal.FilterStatus} object * up-to-date. */ update() { Object.keys(Drupal.filterConfiguration.statuses).forEach((filterID) => { // Update status. Drupal.filterConfiguration.statuses[filterID].active = $(`[name="filters[${filterID}][status]"]`).is(':checked'); // Update current rules. if (Drupal.filterConfiguration.liveSettingParsers[filterID]) { Drupal.filterConfiguration.statuses[filterID].rules = Drupal.filterConfiguration.liveSettingParsers[filterID].getRules(); } }); }, }; /** * Initializes {@link Drupal.filterConfiguration}. * * @type {Drupal~behavior} * * @prop {Drupal~behaviorAttach} attach * Gets filter configuration from filter form input. */ Drupal.behaviors.initializeFilterConfiguration = { attach(context, settings) { const $context = $(context); $context.find('#filters-status-wrapper input.form-checkbox').once('filter-editor-status').each(function () { const $checkbox = $(this); const nameAttribute = $checkbox.attr('name'); // The filter's checkbox has a name attribute of the form // "filters[][status]", parse "" // from it. const filterID = nameAttribute.substring(8, nameAttribute.indexOf(']')); // Create a Drupal.FilterStatus object to track the state (whether it's // active or not and its current settings, if any) of each filter. Drupal.filterConfiguration.statuses[filterID] = new Drupal.FilterStatus(filterID); }); }, }; }(jQuery, _, Drupal, document));
` tags, * but it doesn't make sense to allow for a "summary" attribute on these tags. * Hence these would need to be split in two separate rules. * * HTML rules must be added with the `addHTMLRule()` method. A feature that * has zero HTML rules does not create or modify HTML. * * @constructor * * @param {string} name * The name of the feature. * * @see Drupal.EditorFeatureHTMLRule */ Drupal.EditorFeature = function (name) { this.name = name; this.rules = []; }; /** * Adds a HTML rule to the list of HTML rules for this feature. * * @param {Drupal.EditorFeatureHTMLRule} rule * A text editor feature HTML rule. */ Drupal.EditorFeature.prototype.addHTMLRule = function (rule) { this.rules.push(rule); }; /** * Text filter status object. Initialized with the filter ID. * * Indicates whether the text filter is currently active (enabled) or not. * * Contains a set of HTML rules ({@link Drupal.FilterHTMLRule} objects) that * describe which HTML tags are allowed or forbidden. They can also describe * for a set of tags (or all tags) which attributes, styles and classes are * allowed and which are forbidden. * * It is necessary to allow for multiple HTML rules per feature, for * analogous reasons as {@link Drupal.EditorFeature}. * * HTML rules must be added with the `addHTMLRule()` method. A filter that has * zero HTML rules does not disallow any HTML. * * @constructor * * @param {string} name * The name of the feature. * * @see Drupal.FilterHTMLRule */ Drupal.FilterStatus = function (name) { /** * * @type {string} */ this.name = name; /** * * @type {bool} */ this.active = false; /** * * @type {Array.} */ this.rules = []; }; /** * Adds a HTML rule to the list of HTML rules for this filter. * * @param {Drupal.FilterHTMLRule} rule * A text filter HTML rule. */ Drupal.FilterStatus.prototype.addHTMLRule = function (rule) { this.rules.push(rule); }; /** * A text filter HTML rule object. * * Intended to be used in combination with {@link Drupal.FilterStatus}. * * A text filter rule object describes: * 1. allowed or forbidden tags: (optional) whitelist or blacklist HTML tags * 2. restricted tag properties: (optional) whitelist or blacklist * attributes, styles and classes on a set of HTML tags. * * Typically, each text filter rule object does either 1 or 2, not both. * * The structure can be very clearly seen below: * 1. use the "tags" key to list HTML tags, and set the "allow" key to * either true (to allow these HTML tags) or false (to forbid these HTML * tags). If you leave the "tags" key's default value (the empty array), * no restrictions are applied. * 2. all nested within the "restrictedTags" key: use the "tags" subkey to * list HTML tags to which you want to apply property restrictions, then * use the "allowed" subkey to whitelist specific property values, and * similarly use the "forbidden" subkey to blacklist specific property * values. * * @example *
Whitelist the "p", "strong" and "a" HTML tags.For the "a" HTML tag, only allow the "href" attribute * and the "external" class and disallow the "target" attribute.For all tags, allow the "data-*" attribute (that is, any * attribute that begins with "data-").