UNPKG

@ckeditor/ckeditor5-html-support

Version:

HTML Support feature for CKEditor 5.

256 lines (255 loc) • 10.6 kB
/** * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options */ /** * @module html-support/generalhtmlsupport */ import { Plugin } from 'ckeditor5/src/core.js'; import { toArray } from 'ckeditor5/src/utils.js'; import { DataFilter } from './datafilter.js'; import { CodeBlockElementSupport } from './integrations/codeblock.js'; import { DualContentModelElementSupport } from './integrations/dualcontent.js'; import { HeadingElementSupport } from './integrations/heading.js'; import { ImageElementSupport } from './integrations/image.js'; import { MediaEmbedElementSupport } from './integrations/mediaembed.js'; import { ScriptElementSupport } from './integrations/script.js'; import { TableElementSupport } from './integrations/table.js'; import { StyleElementSupport } from './integrations/style.js'; import { ListElementSupport } from './integrations/list.js'; import { HorizontalLineElementSupport } from './integrations/horizontalline.js'; import { CustomElementSupport } from './integrations/customelement.js'; import { getHtmlAttributeName, modifyGhsAttribute, removeFormatting } from './utils.js'; /** * The General HTML Support feature. * * This is a "glue" plugin which initializes the {@link module:html-support/datafilter~DataFilter data filter} configuration * and features integration with the General HTML Support. */ export class GeneralHtmlSupport extends Plugin { /** * @inheritDoc */ static get pluginName() { return 'GeneralHtmlSupport'; } /** * @inheritDoc */ static get isOfficialPlugin() { return true; } /** * @inheritDoc */ static get requires() { return [ DataFilter, CodeBlockElementSupport, DualContentModelElementSupport, HeadingElementSupport, ImageElementSupport, MediaEmbedElementSupport, ScriptElementSupport, TableElementSupport, StyleElementSupport, ListElementSupport, HorizontalLineElementSupport, CustomElementSupport ]; } /** * @inheritDoc */ init() { const editor = this.editor; const dataFilter = editor.plugins.get(DataFilter); // Load the allowed empty inline elements' configuration. // Note that this modifies DataSchema so must be loaded before registering filtering rules. dataFilter.loadAllowedEmptyElementsConfig(editor.config.get('htmlSupport.allowEmpty') || []); // Load the filtering configuration. dataFilter.loadAllowedConfig(editor.config.get('htmlSupport.allow') || []); dataFilter.loadDisallowedConfig(editor.config.get('htmlSupport.disallow') || []); } /** * @inheritDoc */ afterInit() { const removeFormatCommand = this.editor.commands.get('removeFormat'); removeFormatCommand?.registerCustomAttribute(attributeName => attributeName.startsWith('html') && attributeName.endsWith('Attributes'), removeFormatting); } /** * Returns a GHS model attribute name related to a given view element name. * * @internal * @param viewElementName A view element name. */ getGhsAttributeNameForElement(viewElementName) { const dataSchema = this.editor.plugins.get('DataSchema'); const definitions = Array.from(dataSchema.getDefinitionsForView(viewElementName, false)); const inlineDefinition = definitions.find(definition => (definition.isInline && !definitions[0].isObject)); if (inlineDefinition) { return inlineDefinition.model; } return getHtmlAttributeName(viewElementName); } /** * Updates GHS model attribute for a specified view element name, so it includes the given class name. * * @internal * @param viewElementName A view element name. * @param className The css class to add. * @param selectable The selection or element to update. */ addModelHtmlClass(viewElementName, className, selectable) { const model = this.editor.model; const ghsAttributeName = this.getGhsAttributeNameForElement(viewElementName); model.change(writer => { for (const item of getItemsToUpdateGhsAttribute(model, selectable, ghsAttributeName)) { modifyGhsAttribute(writer, item, ghsAttributeName, 'classes', classes => { for (const value of toArray(className)) { classes.add(value); } }); } }); } /** * Updates GHS model attribute for a specified view element name, so it does not include the given class name. * * @internal * @param viewElementName A view element name. * @param className The css class to remove. * @param selectable The selection or element to update. */ removeModelHtmlClass(viewElementName, className, selectable) { const model = this.editor.model; const ghsAttributeName = this.getGhsAttributeNameForElement(viewElementName); model.change(writer => { for (const item of getItemsToUpdateGhsAttribute(model, selectable, ghsAttributeName)) { modifyGhsAttribute(writer, item, ghsAttributeName, 'classes', classes => { for (const value of toArray(className)) { classes.delete(value); } }); } }); } /** * Updates GHS model attribute for a specified view element name, so it includes the given attribute. * * @param viewElementName A view element name. * @param attributes The object with attributes to set. * @param selectable The selection or element to update. */ setModelHtmlAttributes(viewElementName, attributes, selectable) { const model = this.editor.model; const ghsAttributeName = this.getGhsAttributeNameForElement(viewElementName); model.change(writer => { for (const item of getItemsToUpdateGhsAttribute(model, selectable, ghsAttributeName)) { modifyGhsAttribute(writer, item, ghsAttributeName, 'attributes', attributesMap => { for (const [key, value] of Object.entries(attributes)) { attributesMap.set(key, value); } }); } }); } /** * Updates GHS model attribute for a specified view element name, so it does not include the given attribute. * * @param viewElementName A view element name. * @param attributeName The attribute name (or names) to remove. * @param selectable The selection or element to update. */ removeModelHtmlAttributes(viewElementName, attributeName, selectable) { const model = this.editor.model; const ghsAttributeName = this.getGhsAttributeNameForElement(viewElementName); model.change(writer => { for (const item of getItemsToUpdateGhsAttribute(model, selectable, ghsAttributeName)) { modifyGhsAttribute(writer, item, ghsAttributeName, 'attributes', attributesMap => { for (const key of toArray(attributeName)) { attributesMap.delete(key); } }); } }); } /** * Updates GHS model attribute for a specified view element name, so it includes a given style. * * @param viewElementName A view element name. * @param styles The object with styles to set. * @param selectable The selection or element to update. */ setModelHtmlStyles(viewElementName, styles, selectable) { const model = this.editor.model; const ghsAttributeName = this.getGhsAttributeNameForElement(viewElementName); model.change(writer => { for (const item of getItemsToUpdateGhsAttribute(model, selectable, ghsAttributeName)) { modifyGhsAttribute(writer, item, ghsAttributeName, 'styles', stylesMap => { for (const [key, value] of Object.entries(styles)) { stylesMap.set(key, value); } }); } }); } /** * Updates GHS model attribute for a specified view element name, so it does not include a given style. * * @param viewElementName A view element name. * @param properties The style (or styles list) to remove. * @param selectable The selection or element to update. */ removeModelHtmlStyles(viewElementName, properties, selectable) { const model = this.editor.model; const ghsAttributeName = this.getGhsAttributeNameForElement(viewElementName); model.change(writer => { for (const item of getItemsToUpdateGhsAttribute(model, selectable, ghsAttributeName)) { modifyGhsAttribute(writer, item, ghsAttributeName, 'styles', stylesMap => { for (const key of toArray(properties)) { stylesMap.delete(key); } }); } }); } } /** * Returns an iterator over an items in the selectable that accept given GHS attribute. */ function* getItemsToUpdateGhsAttribute(model, selectable, ghsAttributeName) { if (!selectable) { return; } if (!(Symbol.iterator in selectable) && selectable.is('documentSelection') && selectable.isCollapsed) { if (model.schema.checkAttributeInSelection(selectable, ghsAttributeName)) { yield selectable; } } else { for (const range of getValidRangesForSelectable(model, selectable, ghsAttributeName)) { yield* range.getItems({ shallow: true }); } } } /** * Translates a given selectable to an iterable of ranges. */ function getValidRangesForSelectable(model, selectable, ghsAttributeName) { if (!(Symbol.iterator in selectable) && (selectable.is('node') || selectable.is('$text') || selectable.is('$textProxy'))) { if (model.schema.checkAttribute(selectable, ghsAttributeName)) { return [model.createRangeOn(selectable)]; } else { return []; } } else { return model.schema.getValidRanges(model.createSelection(selectable).getRanges(), ghsAttributeName); } }