fix(tools-api): pasteConfig.tags now supports a sanitize config (#2100)

* event handlers function added

* santization config added

* integrate with paste event

* lint removed

* remove old changes

* object based sanitization configuration support

* paste config updated

* logic updated

* extract tag name from paste-config

* tool tags added

* multi tag sanitization added

* the comments added

* lint removed

* Update types/configs/paste-config.d.ts

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>

* update the changes

* lint removed\

* return empty array by get tags

* submoduble reset

* Update src/components/modules/paste.ts

Co-authored-by: Jorge <46056498+jorgectf@users.noreply.github.com>

* changelog added

* tool comments added

* chore: docs, code comments updated

* fix: xss in processDataTransfer

* base tests added

* test added

* rm 'only' from test suite

* rm log

* reorder test

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
Co-authored-by: Jorge <46056498+jorgectf@users.noreply.github.com>
This commit is contained in:
Umang G. Patel 2022-11-22 01:58:53 +05:30 committed by GitHub
parent cd06bfc9c5
commit f659015be6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 627 additions and 44 deletions

View file

@ -9,6 +9,9 @@
- `Deprecated`*Styles API* — CSS classes `.cdx-settings-button` and `.cdx-settings-button--active` are not recommended to use. Consider configuring your block settings with new JSON API instead.
- `Fix` — Wrong element not highlighted anymore when popover opened.
- `Fix` — When Tunes Menu open keydown events can not be handled inside plugins.
- `Fix` — If a Tool specifies some tags to substitute on paste, all attributes of that tags will be removed before passing them to the tool. Possible XSS vulnerability fixed.
- `Fix` — Workaround for the HTMLJanitor bug with Tables (https://github.com/guardian/html-janitor/issues/3) added
- `Improvement`*Tools API*`pasteConfig().tags` now support sanitizing configuration. It allows you to leave some explicitly specified attributes for pasted content.
### 2.25.0

View file

@ -151,7 +151,7 @@ To handle pasted HTML elements object returned from `pasteConfig` getter should
For correct work you MUST provide `onPaste` handler at least for `defaultBlock` Tool.
> Example
#### Example
Header Tool can handle `H1`-`H6` tags using paste handling API
@ -163,7 +163,27 @@ static get pasteConfig() {
}
```
> Same tag can be handled by one (first specified) Tool only.
**Note. Same tag can be handled by one (first specified) Tool only.**
**Note. All attributes of pasted tag will be removed. To leave some attribute, you should explicitly specify them. Se below**
Let's suppose you want to leave the 'src' attribute when handle pasting of the `img` tags. Your config should look like this:
```javascript
static get pasteConfig() {
return {
tags: [
{
img: {
src: true
}
}
],
}
}
```
[Read more](https://editorjs.io/sanitizer) about the sanitizing configuration.
### RegExp patterns handling

View file

@ -53,7 +53,7 @@ export default class Dom {
*
* @returns {HTMLElement}
*/
public static make(tagName: string, classNames: string|string[] = null, attributes: object = {}): HTMLElement {
public static make(tagName: string, classNames: string | string[] = null, attributes: object = {}): HTMLElement {
const el = document.createElement(tagName);
if (Array.isArray(classNames)) {
@ -109,8 +109,8 @@ export default class Dom {
* @param {Element|Element[]|DocumentFragment|Text|Text[]} elements - element or elements list
*/
public static append(
parent: Element|DocumentFragment,
elements: Element|Element[]|DocumentFragment|Text|Text[]
parent: Element | DocumentFragment,
elements: Element | Element[] | DocumentFragment | Text | Text[]
): void {
if (Array.isArray(elements)) {
elements.forEach((el) => parent.appendChild(el));
@ -125,7 +125,7 @@ export default class Dom {
* @param {Element} parent - where to append
* @param {Element|Element[]} elements - element or elements list
*/
public static prepend(parent: Element, elements: Element|Element[]): void {
public static prepend(parent: Element, elements: Element | Element[]): void {
if (Array.isArray(elements)) {
elements = elements.reverse();
elements.forEach((el) => parent.prepend(el));
@ -168,7 +168,7 @@ export default class Dom {
*
* @returns {Element}
*/
public static find(el: Element|Document = document, selector: string): Element {
public static find(el: Element | Document = document, selector: string): Element {
return el.querySelector(selector);
}
@ -192,7 +192,7 @@ export default class Dom {
*
* @returns {NodeList}
*/
public static findAll(el: Element|Document = document, selector: string): NodeList {
public static findAll(el: Element | Document = document, selector: string): NodeList {
return el.querySelectorAll(selector);
}
@ -523,6 +523,8 @@ export default class Dom {
'ruby',
'section',
'table',
'tbody',
'thead',
'tr',
'tfoot',
'ul',
@ -619,7 +621,7 @@ export default class Dom {
* @todo handle case when editor initialized in scrollable popup
* @param el - element to compute offset
*/
public static offset(el): {top: number; left: number; right: number; bottom: number} {
public static offset(el): { top: number; left: number; right: number; bottom: number } {
const rect = el.getBoundingClientRect();
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;

View file

@ -4,7 +4,9 @@ import * as _ from '../utils';
import {
BlockAPI,
PasteEvent,
PasteEventDetail
PasteEventDetail,
SanitizerConfig,
SanitizerRule
} from '../../../types';
import Block from '../block';
import { SavedData } from '../../../types/data-formats';
@ -20,6 +22,12 @@ interface TagSubstitute {
*
*/
tool: BlockTool;
/**
* If a Tool specifies just a tag name, all the attributes will be sanitized.
* But Tool can explicitly specify sanitizer configuration for supported tags
*/
sanitizationConfig?: SanitizerRule;
}
/**
@ -112,12 +120,12 @@ export default class Paste extends Module {
/**
* Tags` substitutions parameters
*/
private toolsTags: {[tag: string]: TagSubstitute} = {};
private toolsTags: { [tag: string]: TagSubstitute } = {};
/**
* Store tags to substitute by tool name
*/
private tagsByTool: {[tools: string]: string[]} = {};
private tagsByTool: { [tools: string]: string[] } = {};
/** Patterns` substitutions parameters */
private toolsPatterns: PatternSubstitute[] = [];
@ -186,7 +194,7 @@ export default class Paste extends Module {
this.insertEditorJSData(JSON.parse(editorJSData));
return;
} catch (e) {} // Do nothing and continue execution as usual if error appears
} catch (e) { } // Do nothing and continue execution as usual if error appears
}
/**
@ -198,7 +206,11 @@ export default class Paste extends Module {
/** Add all tags that can be substituted to sanitizer configuration */
const toolsTags = Object.keys(this.toolsTags).reduce((result, tag) => {
result[tag.toLowerCase()] = true;
/**
* If Tool explicitly specifies sanitizer configuration for the tag, use it.
* Otherwise, remove all attributes
*/
result[tag.toLowerCase()] = this.toolsTags[tag].sanitizationConfig ?? {};
return result;
}, {});
@ -306,31 +318,69 @@ export default class Paste extends Module {
}
}
/**
* Get tags name list from either tag name or sanitization config.
*
* @param {string | object} tagOrSanitizeConfig - tag name or sanitize config object.
* @returns {string[]} array of tags.
*/
private collectTagNames(tagOrSanitizeConfig: string | SanitizerConfig): string[] {
/**
* If string, then it is a tag name.
*/
if (_.isString(tagOrSanitizeConfig)) {
return [ tagOrSanitizeConfig ];
}
/**
* If object, then its keys are tags.
*/
if (_.isObject(tagOrSanitizeConfig)) {
return Object.keys(tagOrSanitizeConfig);
}
/** Return empty tag list */
return [];
}
/**
* Get tags to substitute by Tool
*
* @param tool - BlockTool object
*/
private getTagsConfig(tool: BlockTool): void {
const tags = tool.pasteConfig.tags || [];
const tagsOrSanitizeConfigs = tool.pasteConfig.tags || [];
const toolTags = [];
tags.forEach((tag) => {
if (Object.prototype.hasOwnProperty.call(this.toolsTags, tag)) {
_.log(
`Paste handler for «${tool.name}» Tool on «${tag}» tag is skipped ` +
`because it is already used by «${this.toolsTags[tag].tool.name}» Tool.`,
'warn'
);
tagsOrSanitizeConfigs.forEach((tagOrSanitizeConfig) => {
const tags = this.collectTagNames(tagOrSanitizeConfig);
return;
}
/**
* Add tags to toolTags array
*/
toolTags.push(...tags);
tags.forEach((tag) => {
if (Object.prototype.hasOwnProperty.call(this.toolsTags, tag)) {
_.log(
`Paste handler for «${tool.name}» Tool on «${tag}» tag is skipped ` +
`because it is already used by «${this.toolsTags[tag].tool.name}» Tool.`,
'warn'
);
this.toolsTags[tag.toUpperCase()] = {
tool,
};
return;
}
/**
* Get sanitize config for tag.
*/
const sanitizationConfig = _.isObject(tagOrSanitizeConfig) ? tagOrSanitizeConfig[tag] : null;
this.toolsTags[tag.toUpperCase()] = {
tool,
sanitizationConfig,
};
});
});
this.tagsByTool[tool.name] = tags.map((t) => t.toUpperCase());
this.tagsByTool[tool.name] = toolTags.map((t) => t.toUpperCase());
}
/**
@ -449,7 +499,7 @@ export default class Paste extends Module {
private async processFiles(items: FileList): Promise<void> {
const { BlockManager } = this.Editor;
let dataToInsert: {type: string; event: PasteEvent}[];
let dataToInsert: { type: string; event: PasteEvent }[];
dataToInsert = await Promise.all(
Array
@ -473,7 +523,7 @@ export default class Paste extends Module {
*
* @param {File} file - file to process
*/
private async processFile(file: File): Promise<{event: PasteEvent; type: string}> {
private async processFile(file: File): Promise<{ event: PasteEvent; type: string }> {
const extension = _.getFileExtension(file);
const foundConfig = Object
@ -515,6 +565,19 @@ export default class Paste extends Module {
*/
private processHTML(innerHTML: string): PasteData[] {
const { Tools } = this.Editor;
/**
* @todo Research, do we really need to always wrap innerHTML to a div:
* - <img> tag could be processed separately, but for now it becomes div-wrapped
* and then .getNodes() returns strange: [document-fragment, img]
* (description of the method says that it should should return only block tags or fragments,
* but there are inline-block element along with redundant empty fragment)
* - probably this is a reason of bugs with unexpected new block creation instead of inline pasting:
* - https://github.com/codex-team/editor.js/issues/1427
* - https://github.com/codex-team/editor.js/issues/1244
* - https://github.com/codex-team/editor.js/issues/740
*
*/
const wrapper = $.make('DIV');
wrapper.innerHTML = innerHTML;
@ -543,16 +606,65 @@ export default class Paste extends Module {
break;
}
const { tags } = tool.pasteConfig;
const { tags: tagsOrSanitizeConfigs } = tool.pasteConfig;
const toolTags = tags.reduce((result, tag) => {
result[tag.toLowerCase()] = {};
/**
* Reduce the tags or sanitize configs to a single array of sanitize config.
* For example:
* If sanitize config is
* [ 'tbody',
* {
* table: {
* width: true,
* height: true,
* },
* },
* {
* td: {
* colspan: true,
* rowspan: true,
* },
* tr: { // <-- the second tag
* height: true,
* },
* },
* ]
* then sanitize config will be
* [
* 'table':{},
* 'tbody':{width: true, height: true}
* 'td':{colspan: true, rowspan: true},
* 'tr':{height: true}
* ]
*/
const toolTags = tagsOrSanitizeConfigs.reduce((result, tagOrSanitizeConfig) => {
const tags = this.collectTagNames(tagOrSanitizeConfig);
tags.forEach((tag) => {
const sanitizationConfig = _.isObject(tagOrSanitizeConfig) ? tagOrSanitizeConfig[tag] : null;
result[tag] = sanitizationConfig || {};
});
return result;
}, {});
const customConfig = Object.assign({}, toolTags, tool.baseSanitizeConfig);
content.innerHTML = clean(content.innerHTML, customConfig);
/**
* A workaround for the HTMLJanitor bug with Tables (incorrect sanitizing of table.innerHTML)
* https://github.com/guardian/html-janitor/issues/3
*/
if (content.tagName.toLowerCase() === 'table') {
const cleanTableHTML = clean(content.outerHTML, customConfig);
const tmpWrapper = $.make('div', undefined, {
innerHTML: cleanTableHTML,
});
content = tmpWrapper.firstChild;
} else {
content.innerHTML = clean(content.innerHTML, customConfig);
}
const event = this.composePasteEvent('tag', {
data: content,
@ -565,7 +677,12 @@ export default class Paste extends Module {
event,
};
})
.filter((data) => !$.isNodeEmpty(data.content) || $.isSingleTag(data.content));
.filter((data) => {
const isEmpty = $.isEmpty(data.content);
const isSingleTag = $.isSingleTag(data.content);
return !isEmpty || isSingleTag;
});
}
/**
@ -576,7 +693,7 @@ export default class Paste extends Module {
* @returns {PasteData[]}
*/
private processPlain(plain: string): PasteData[] {
const { defaultBlock } = this.config as {defaultBlock: string};
const { defaultBlock } = this.config as { defaultBlock: string };
if (!plain) {
return [];
@ -681,7 +798,7 @@ export default class Paste extends Module {
*
* @returns {Promise<{event: PasteEvent, tool: string}>}
*/
private async processPattern(text: string): Promise<{event: PasteEvent; tool: string}> {
private async processPattern(text: string): Promise<{ event: PasteEvent; tool: string }> {
const pattern = this.toolsPatterns.find((substitute) => {
const execResult = substitute.pattern.exec(text);
@ -878,3 +995,4 @@ export default class Paste extends Module {
}) as PasteEvent;
}
}

View file

@ -1,5 +1,5 @@
import { ToolboxConfig, BlockToolData, ToolboxConfigEntry } from '../../../../types';
import { TunesMenuConfig } from '../../../../types/tools';
import { ToolboxConfig, BlockToolData, ToolboxConfigEntry, PasteConfig } from '../../../../types';
import { HTMLPasteEvent, PasteEvent, TunesMenuConfig } from '../../../../types/tools';
/* eslint-disable @typescript-eslint/no-empty-function */
@ -270,7 +270,7 @@ describe('Editor Tools Api', () => {
});
});
context('Tunes', () => {
context('Tunes — renderSettings()', () => {
it('should contain a single block tune configured in tool\'s renderSettings() method', () => {
/** Tool with single tunes menu entry configured */
class TestTool {
@ -490,4 +490,431 @@ describe('Editor Tools Api', () => {
.should('contain.text', sampleText);
});
});
/**
* @todo cover all the pasteConfig properties
*/
context('Paste — pasteConfig()', () => {
context('tags', () => {
/**
* tags: ['H1', 'H2']
*/
it('should use corresponding tool when the array of tag names specified', () => {
/**
* Test tool with pasteConfig.tags specified
*/
class TestImgTool {
/** config specified handled tag */
public static get pasteConfig(): PasteConfig {
return {
tags: [ 'img' ], // only tag name specified. Attributes should be sanitized
};
}
/** onPaste callback will be stubbed below */
public onPaste(): void {}
/** save is required for correct implementation of the BlockTool class */
public save(): void {}
/** render is required for correct implementation of the BlockTool class */
public render(): HTMLElement {
return document.createElement('img');
}
}
const toolsOnPaste = cy.spy(TestImgTool.prototype, 'onPaste');
cy.createEditor({
tools: {
testTool: TestImgTool,
},
}).as('editorInstance');
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.click()
.paste({
'text/html': '<img>',
})
.then(() => {
expect(toolsOnPaste).to.be.called;
});
});
/**
* tags: ['img'] -> <img>
*/
it('should sanitize all attributes from tag, if only tag name specified ', () => {
/**
* Variable used for spying the pasted element we are passing to the Tool
*/
let pastedElement;
/**
* Test tool with pasteConfig.tags specified
*/
class TestImageTool {
/** config specified handled tag */
public static get pasteConfig(): PasteConfig {
return {
tags: [ 'img' ], // only tag name specified. Attributes should be sanitized
};
}
/** onPaste callback will be stubbed below */
public onPaste(): void {}
/** save is required for correct implementation of the BlockTool class */
public save(): void {}
/** render is required for correct implementation of the BlockTool class */
public render(): HTMLElement {
return document.createElement('img');
}
}
/**
* Stub the onPaste method to access the PasteEvent data for assertion
*/
cy.stub(TestImageTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => {
pastedElement = event.detail.data;
});
cy.createEditor({
tools: {
testImageTool: TestImageTool,
},
});
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.click()
.paste({
'text/html': '<img src="foo" onerror="alert(123)"/>', // all attributes should be sanitized
})
.then(() => {
expect(pastedElement).not.to.be.undefined;
expect(pastedElement.tagName.toLowerCase()).eq('img');
expect(pastedElement.attributes.length).eq(0);
});
});
/**
* tags: [{
* img: {
* src: true
* }
* }]
* -> <img src="">
*
*/
it('should leave attributes if entry specified as a sanitizer config ', () => {
/**
* Variable used for spying the pasted element we are passing to the Tool
*/
let pastedElement;
/**
* Test tool with pasteConfig.tags specified
*/
class TestImageTool {
/** config specified handled tag */
public static get pasteConfig(): PasteConfig {
return {
tags: [
{
img: {
src: true,
},
},
],
};
}
/** onPaste callback will be stubbed below */
public onPaste(): void {}
/** save is required for correct implementation of the BlockTool class */
public save(): void {}
/** render is required for correct implementation of the BlockTool class */
public render(): HTMLElement {
return document.createElement('img');
}
}
/**
* Stub the onPaste method to access the PasteEvent data for assertion
*/
cy.stub(TestImageTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => {
pastedElement = event.detail.data;
});
cy.createEditor({
tools: {
testImageTool: TestImageTool,
},
});
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.click()
.paste({
'text/html': '<img src="foo" onerror="alert(123)"/>',
})
.then(() => {
expect(pastedElement).not.to.be.undefined;
/**
* Check that the <img> has only "src" attribute
*/
expect(pastedElement.tagName.toLowerCase()).eq('img');
expect(pastedElement.getAttribute('src')).eq('foo');
expect(pastedElement.attributes.length).eq(1);
});
});
/**
* tags: [
* 'video',
* {
* source: {
* src: true
* }
* }
* ]
*/
it('should support mixed tag names and sanitizer config ', () => {
/**
* Variable used for spying the pasted element we are passing to the Tool
*/
let pastedElement;
/**
* Test tool with pasteConfig.tags specified
*/
class TestTool {
/** config specified handled tag */
public static get pasteConfig(): PasteConfig {
return {
tags: [
'video', // video should not have attributes
{
source: { // source should have only src attribute
src: true,
},
},
],
};
}
/** onPaste callback will be stubbed below */
public onPaste(): void {}
/** save is required for correct implementation of the BlockTool class */
public save(): void {}
/** render is required for correct implementation of the BlockTool class */
public render(): HTMLElement {
return document.createElement('tbody');
}
}
/**
* Stub the onPaste method to access the PasteEvent data for assertion
*/
cy.stub(TestTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => {
pastedElement = event.detail.data;
});
cy.createEditor({
tools: {
testTool: TestTool,
},
});
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.click()
.paste({
'text/html': '<video width="100"><source src="movie.mp4" type="video/mp4"></video>',
})
.then(() => {
expect(pastedElement).not.to.be.undefined;
/**
* Check that <video> has no attributes
*/
expect(pastedElement.tagName.toLowerCase()).eq('video');
expect(pastedElement.attributes.length).eq(0);
/**
* Check that the <source> has only 'src' attribute
*/
expect(pastedElement.firstChild.tagName.toLowerCase()).eq('source');
expect(pastedElement.firstChild.getAttribute('src')).eq('movie.mp4');
expect(pastedElement.firstChild.attributes.length).eq(1);
});
});
/**
* tags: [
* {
* td: { width: true },
* tr: { height: true }
* }
* ]
*/
it('should support config with several keys as the single entry', () => {
/**
* Variable used for spying the pasted element we are passing to the Tool
*/
let pastedElement;
/**
* Test tool with pasteConfig.tags specified
*/
class TestTool {
/** config specified handled tag */
public static get pasteConfig(): PasteConfig {
return {
tags: [
{
video: {
width: true,
},
source: {
src: true,
},
},
],
};
}
/** onPaste callback will be stubbed below */
public onPaste(): void {}
/** save is required for correct implementation of the BlockTool class */
public save(): void {}
/** render is required for correct implementation of the BlockTool class */
public render(): HTMLElement {
return document.createElement('tbody');
}
}
/**
* Stub the onPaste method to access the PasteEvent data for assertion
*/
cy.stub(TestTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => {
pastedElement = event.detail.data;
});
cy.createEditor({
tools: {
testTool: TestTool,
},
});
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.click()
.paste({
'text/html': '<video width="100"><source src="movie.mp4" type="video/mp4"></video>',
})
.then(() => {
expect(pastedElement).not.to.be.undefined;
expect(pastedElement.tagName.toLowerCase()).eq('video');
/**
* Check that the <tr> has the 'height' attribute
*/
expect(pastedElement.firstChild.tagName.toLowerCase()).eq('source');
expect(pastedElement.firstChild.getAttribute('src')).eq('movie.mp4');
});
});
/**
* It covers a workaround HTMLJanitor bug with tables (incorrect sanitizing of table.innerHTML)
* https://github.com/guardian/html-janitor/issues/3
*/
it('should correctly sanitize Table structure (test for HTMLJanitor bug)', () => {
/**
* Variable used for spying the pasted element we are passing to the Tool
*/
let pastedElement;
/**
* Test tool with pasteConfig.tags specified
*/
class TestTool {
/** config specified handled tag */
public static get pasteConfig(): PasteConfig {
return {
tags: [
'table',
'tbody',
{
td: {
width: true,
},
tr: {
height: true,
},
},
],
};
}
/** onPaste callback will be stubbed below */
public onPaste(): void {}
/** save is required for correct implementation of the BlockTool class */
public save(): void {}
/** render is required for correct implementation of the BlockTool class */
public render(): HTMLElement {
return document.createElement('tbody');
}
}
/**
* Stub the onPaste method to access the PasteEvent data for assertion
*/
cy.stub(TestTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => {
pastedElement = event.detail.data;
});
cy.createEditor({
tools: {
testTool: TestTool,
},
});
cy.get('[data-cy=editorjs]')
.get('div.ce-block')
.click()
.paste({
'text/html': '<table><tr height="50"><td width="300">Ho-Ho-Ho</td></tr></table>',
})
.then(() => {
expect(pastedElement).not.to.be.undefined;
expect(pastedElement.tagName.toLowerCase()).eq('table');
/**
* Check that the <tr> has the 'height' attribute
*/
expect(pastedElement.querySelector('tr')).not.to.be.undefined;
expect(pastedElement.querySelector('tr').getAttribute('height')).eq('50');
/**
* Check that the <td> has the 'width' attribute
*/
expect(pastedElement.querySelector('td')).not.to.be.undefined;
expect(pastedElement.querySelector('td').getAttribute('width')).eq('300');
});
});
});
});
});

View file

@ -1,12 +1,22 @@
import { SanitizerConfig } from "./sanitizer-config";
/**
* Tool onPaste configuration object
*/
export interface PasteConfig {
/**
* Array of tags Tool can substitute
* Array of tags Tool can substitute.
*
* Could also contain a sanitize-config if you need to save some tag's attribute.
* For example:
* [
* {
* img: { src: true },
* }
* ],
* @type string[]
*/
tags?: string[];
tags?: (string | SanitizerConfig)[];
/**
* Object of string patterns Tool can substitute.

View file

@ -2,7 +2,9 @@
* Sanitizer config of each HTML element
* @see {@link https://github.com/guardian/html-janitor#options}
*/
type TagConfig = boolean | { [attr: string]: boolean | string };
export type TagConfig = boolean | { [attr: string]: boolean | string };
export type SanitizerRule = TagConfig | ((el: Element) => TagConfig)
export interface SanitizerConfig {
/**
@ -37,5 +39,5 @@ export interface SanitizerConfig {
* }
* }
*/
[key: string]: TagConfig | ((el: Element) => TagConfig);
[key: string]: SanitizerRule;
}

1
types/index.d.ts vendored
View file

@ -64,6 +64,7 @@ export {BlockTune, BlockTuneConstructable} from './block-tunes';
export {
EditorConfig,
SanitizerConfig,
SanitizerRule,
PasteConfig,
LogLevels,
ConversionConfig,