diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 72acee3cb..19955d05b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,7 @@ - `Improvement` - `paste` config supports filter functions for tag-based substitution - `Fix` - "Convert to" is not shown in the Inline Toolbar when there are no tools to convert to - `Fix` - Invoke tool `reset()` methods on `Tools.destroy()`, catching and logging any errors as warnings +- `Fix` - Inline formatting is no longer removed when a Block is converted to, or merged into, a Tool with per-field `sanitize` rules ### 2.31.7 diff --git a/package.json b/package.json index b57e61968..9e8894910 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.32.0-rc.3", + "version": "2.32.0-rc.4", "description": "Editor.js — open source block-style WYSIWYG editor with JSON output", "main": "dist/editorjs.umd.js", "module": "dist/editorjs.mjs", diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts index be8e1e247..2bb80ed4a 100644 --- a/src/components/modules/blockManager.ts +++ b/src/components/modules/blockManager.ts @@ -9,7 +9,7 @@ import Module from '../__module'; import $ from '../dom'; import * as _ from '../utils'; import Blocks from '../blocks'; -import type { BlockToolData, PasteEvent } from '../../../types'; +import type { BlockToolData, PasteEvent, ToolConfig } from '../../../types'; import type { BlockTuneData } from '../../../types/block-tunes/block-tune-data'; import BlockAPI from '../block/api'; import type { BlockMutationEventMap, BlockMutationType } from '../../../types/events/block'; @@ -21,6 +21,7 @@ import { BlockChanged } from '../events'; import { clean, sanitizeBlocks } from '../utils/sanitizer'; import { convertStringToBlockData, isBlockConvertable } from '../utils/blocks'; import PromiseQueue from '../utils/promise-queue'; +import type BlockToolAdapter from '../tools/block'; /** * @typedef {BlockManager} BlockManager @@ -502,9 +503,8 @@ export default class BlockManager extends Module { */ } else if (targetBlock.mergeable && isBlockConvertable(blockToMerge, 'export') && isBlockConvertable(targetBlock, 'import')) { const blockToMergeDataStringified = await blockToMerge.exportDataAsString(); - const cleanData = clean(blockToMergeDataStringified, targetBlock.tool.sanitizeConfig); - blockToMergeData = convertStringToBlockData(cleanData, targetBlock.tool.conversionConfig); + blockToMergeData = this.composeSanitizedBlockData(blockToMergeDataStringified, targetBlock.tool); } if (blockToMergeData === undefined) { @@ -853,17 +853,9 @@ export default class BlockManager extends Module { const exportedData = await blockToConvert.exportDataAsString(); /** - * Clean exported data with replacing sanitizer config + * Now using Conversion Config "import" we compose a new Block data, cleaned with the replacing Tool's sanitizer config */ - const cleanData: string = clean( - exportedData, - replacingTool.sanitizeConfig - ); - - /** - * Now using Conversion Config "import" we compose a new Block data - */ - let newBlockData = convertStringToBlockData(cleanData, replacingTool.conversionConfig, replacingTool.settings); + let newBlockData = this.composeSanitizedBlockData(exportedData, replacingTool, replacingTool.settings); /** * Optional data overrides. @@ -927,6 +919,34 @@ export default class BlockManager extends Module { })); } + /** + * Composes data of the passed Tool from a string exported by another Block and sanitizes it. + * + * The Tool's sanitizer config describes its data fields ({ text: { b: true }, caption: false }), + * so it is applied to the composed data, as on saving. Applied to the exported string, + * it would treat the field names as tag names and remove every real tag. + * + * @param exportedString - string got with the Conversion Config "export" + * @param tool - Tool to compose data for + * @param toolConfig - user config of the Tool, passed to the Conversion Config "import" function + */ + private composeSanitizedBlockData(exportedString: string, tool: BlockToolAdapter, toolConfig?: ToolConfig): BlockToolData { + /** + * A Tool without any sanitizer rules allows no tags. sanitizeBlocks() skips an empty config, so clean the string itself + */ + if (_.isEmpty(tool.sanitizeConfig)) { + return convertStringToBlockData(clean(exportedString), tool.conversionConfig, toolConfig); + } + + const data = convertStringToBlockData(exportedString, tool.conversionConfig, toolConfig); + const [ sanitized ] = sanitizeBlocks([ { + tool: tool.name, + data, + } ], tool.sanitizeConfig); + + return sanitized.data; + } + /** * Bind Block events * diff --git a/test/cypress/tests/api/blocks.cy.ts b/test/cypress/tests/api/blocks.cy.ts index ab97b8b5c..86a1dd0a4 100644 --- a/test/cypress/tests/api/blocks.cy.ts +++ b/test/cypress/tests/api/blocks.cy.ts @@ -1,5 +1,5 @@ import type EditorJS from '../../../../types/index'; -import type { ConversionConfig, ToolboxConfig, ToolConfig } from '../../../../types'; +import type { ConversionConfig, SanitizerConfig, ToolboxConfig, ToolConfig } from '../../../../types'; import ToolMock, { type MockToolData } from '../../fixtures/tools/ToolMock'; import { nanoid } from 'nanoid'; @@ -361,6 +361,64 @@ describe('api.blocks', () => { }); }); + it('should keep inline formatting allowed by the target Tool sanitize config for the imported field', function () { + /** + * Mock of Tool whose sanitize config describes its data fields + */ + class ConvertableTool extends ToolMock { + /** + * Specify how to import string data to this Tool + */ + public static get conversionConfig(): ConversionConfig { + return { + import: 'text', + }; + } + + /** + * Rules for the "text" field: only bold is allowed + */ + public static get sanitize(): SanitizerConfig { + return { + text: { + b: true, + }, + }; + } + } + + const existingBlock = { + id: 'test-id-123', + type: 'paragraph', + data: { + text: 'Some bold and underlined text', + }, + }; + + cy.createEditor({ + tools: { + convertableTool: { + class: ConvertableTool, + inlineToolbar: false, + }, + }, + data: { + blocks: [ + existingBlock, + ], + }, + }).then(async (editor) => { + await editor.blocks.convert(existingBlock.id, 'convertableTool'); + + /** + * Check the rendered Block: saving sanitizes the data once more and would hide the data passed to the Tool + */ + cy.get('[data-cy=editorjs]') + .find('.ce-block__content [contenteditable]') + .should('have.html', 'Some bold and underlined text'); + }); + }); + it('should throw an error if nonexisting Block id passed', function () { cy.createEditor({}) .then((editor) => {