From 17e8d67f590e8e9f666d6eb5a69ee53823d450a2 Mon Sep 17 00:00:00 2001 From: Zihua Li Date: Fri, 15 Mar 2024 00:25:47 +0800 Subject: [PATCH] Add formats option for specifying allowed formats --- CHANGELOG.md | 1 + packages/quill/src/core/quill.ts | 22 +++++- .../core/utils/createRegistryWithFormats.ts | 40 ++++++++++ packages/quill/test/unit/core/quill.spec.ts | 75 ++++++++++++++++++- .../utils/createRegistryWithFormats.spec.ts | 33 ++++++++ .../website/content/docs/configuration.mdx | 38 ++++++++++ 6 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 packages/quill/src/core/utils/createRegistryWithFormats.ts create mode 100644 packages/quill/test/unit/core/utils/createRegistryWithFormats.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b321c7c79..d15707d984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Fix `Quill#getSemanticHTML()` for list items - Remove unnecessary Firefox workaround - **Clipboard** Fix redundant newlines when pasting from external sources +- Add `formats` option for specifying allowed formats # 2.0.0-rc.2 diff --git a/packages/quill/src/core/quill.ts b/packages/quill/src/core/quill.ts index 140d5b21c1..85bd1f6c10 100644 --- a/packages/quill/src/core/quill.ts +++ b/packages/quill/src/core/quill.ts @@ -23,6 +23,7 @@ import Theme from './theme.js'; import type { ThemeConstructor } from './theme.js'; import scrollRectIntoView from './utils/scrollRectIntoView.js'; import type { Rect } from './utils/scrollRectIntoView.js'; +import createRegistryWithFormats from './utils/createRegistryWithFormats.js'; const debug = logger('quill'); @@ -37,9 +38,10 @@ interface Options { placeholder?: string; bounds?: HTMLElement | string | null; modules?: Record; + formats?: string[] | null; } -interface ExpandedOptions extends Omit { +interface ExpandedOptions extends Omit { theme: ThemeConstructor; registry: Parchment.Registry; container: HTMLElement; @@ -785,8 +787,26 @@ function expandConfig( const config = { ...quillDefaults, ...themeDefaults, ...options }; + let registry = options.registry; + if (registry) { + if (options.formats) { + debug.warn('Ignoring "formats" option because "registry" is specified'); + } + } else { + registry = options.formats + ? createRegistryWithFormats( + options.formats, + config.registry, + (errorMessage) => { + debug.error(errorMessage); + }, + ) + : config.registry; + } + return { ...config, + registry, container, theme, modules: Object.entries(modules).reduce( diff --git a/packages/quill/src/core/utils/createRegistryWithFormats.ts b/packages/quill/src/core/utils/createRegistryWithFormats.ts new file mode 100644 index 0000000000..3e7360f58e --- /dev/null +++ b/packages/quill/src/core/utils/createRegistryWithFormats.ts @@ -0,0 +1,40 @@ +import { Registry } from 'parchment'; + +const MAX_REGISTER_ITERATIONS = 100; +const CORE_FORMATS = ['block', 'break', 'cursor', 'inline', 'scroll', 'text']; + +const createRegistryWithFormats = ( + formats: string[], + sourceRegistry: Registry, + logError: (errorMessage: string) => void, +) => { + const registry = new Registry(); + CORE_FORMATS.forEach((name) => { + const coreBlot = sourceRegistry.query(name); + if (coreBlot) registry.register(coreBlot); + }); + + formats.forEach((name) => { + let format = sourceRegistry.query(name); + if (!format) { + logError( + `Cannot register "${name}" specified in "formats" config. Are you sure it was registered?`, + ); + } + let iterations = 0; + while (format) { + registry.register(format); + format = 'blotName' in format ? format.requiredContainer ?? null : null; + + iterations += 1; + if (iterations > MAX_REGISTER_ITERATIONS) { + logError(`Maximum iterations reached when registering "${name}"`); + break; + } + } + }); + + return registry; +}; + +export default createRegistryWithFormats; diff --git a/packages/quill/test/unit/core/quill.spec.ts b/packages/quill/test/unit/core/quill.spec.ts index 6dda6b881c..9680b200a8 100644 --- a/packages/quill/test/unit/core/quill.spec.ts +++ b/packages/quill/test/unit/core/quill.spec.ts @@ -1,11 +1,16 @@ import '../../../src/quill.js'; import Delta from 'quill-delta'; +import { Registry } from 'parchment'; import { beforeEach, describe, expect, test, vitest } from 'vitest'; import type { MockedFunction } from 'vitest'; import Emitter from '../../../src/core/emitter.js'; import Theme from '../../../src/core/theme.js'; import Toolbar from '../../../src/modules/toolbar.js'; -import Quill, { expandConfig, overload } from '../../../src/core/quill.js'; +import Quill, { + expandConfig, + globalRegistry, + overload, +} from '../../../src/core/quill.js'; import { Range } from '../../../src/core/selection.js'; import Snow from '../../../src/themes/snow.js'; import { normalizeHTML } from '../__helpers__/utils.js'; @@ -779,6 +784,74 @@ describe('Quill', () => { Toolbar.DEFAULTS.handlers.clean, ); }); + + test('registry defaults to globalRegistry', () => { + const config = expandConfig(`#${testContainerId}`, {}); + expect(config.registry).toBe(globalRegistry); + }); + + describe('formats', () => { + test('null value allows all formats', () => { + const config = expandConfig(`#${testContainerId}`, { + formats: null, + }); + + expect(config.registry.query('cursor')).toBeTruthy(); + expect(config.registry.query('bold')).toBeTruthy(); + }); + + test('always allows core formats', () => { + const config = expandConfig(`#${testContainerId}`, { + formats: ['bold'], + }); + + expect(config.registry.query('cursor')).toBeTruthy(); + expect(config.registry.query('break')).toBeTruthy(); + }); + + test('limits allowed formats', () => { + const config = expandConfig(`#${testContainerId}`, { + formats: ['bold'], + }); + + expect(config.registry.query('italic')).toBeFalsy(); + expect(config.registry.query('bold')).toBeTruthy(); + }); + + test('ignores unknown formats', () => { + const name = 'my-unregistered-format'; + const config = expandConfig(`#${testContainerId}`, { + formats: [name], + }); + + expect(config.registry.query(name)).toBeFalsy(); + }); + + test('registers list container when there is a list', () => { + expect( + expandConfig(`#${testContainerId}`, { + formats: ['bold'], + }).registry.query('list-container'), + ).toBeFalsy(); + + expect( + expandConfig(`#${testContainerId}`, { + formats: ['list'], + }).registry.query('list-container'), + ).toBeTruthy(); + }); + + test('provides both registry and formats', () => { + const registry = new Registry(); + const config = expandConfig(`#${testContainerId}`, { + registry, + formats: ['bold'], + }); + + expect(config.registry).toBe(registry); + expect(config.registry.query('bold')).toBeFalsy(); + }); + }); }); describe('overload', () => { diff --git a/packages/quill/test/unit/core/utils/createRegistryWithFormats.spec.ts b/packages/quill/test/unit/core/utils/createRegistryWithFormats.spec.ts new file mode 100644 index 0000000000..a152ecd91f --- /dev/null +++ b/packages/quill/test/unit/core/utils/createRegistryWithFormats.spec.ts @@ -0,0 +1,33 @@ +import '../../../../src/quill.js'; +import { describe, expect, test, vitest } from 'vitest'; +import createRegistryWithFormats from '../../../../src/core/utils/createRegistryWithFormats.js'; +import { globalRegistry } from '../../../../src/core/quill.js'; + +describe('createRegistryWithFormats', () => { + test('register core formats', () => { + const registry = createRegistryWithFormats([], globalRegistry, () => {}); + expect(registry.query('cursor')).toBeTruthy(); + expect(registry.query('bold')).toBeFalsy(); + }); + + test('register specified formats', () => { + const registry = createRegistryWithFormats( + ['bold'], + globalRegistry, + () => {}, + ); + expect(registry.query('cursor')).toBeTruthy(); + expect(registry.query('bold')).toBeTruthy(); + }); + + test('report missing formats', () => { + const logError = vitest.fn(); + const registry = createRegistryWithFormats( + ['my-unknown'], + globalRegistry, + logError, + ); + expect(registry.query('my-unknown')).toBeFalsy(); + expect(logError).toHaveBeenCalledWith(expect.stringMatching('my-unknown')); + }); +}); diff --git a/packages/website/content/docs/configuration.mdx b/packages/website/content/docs/configuration.mdx index 53b54acd39..ef83e00429 100644 --- a/packages/website/content/docs/configuration.mdx +++ b/packages/website/content/docs/configuration.mdx @@ -104,3 +104,41 @@ quill.setContents( ### theme Name of theme to use. The builtin options are `"bubble"` or `"snow"`. An invalid or falsy value will load a default minimal theme. Note the theme's specific stylesheet still needs to be included manually. See [Themes](/docs/themes/) for more information. + +### formats + +Default: `null` + +A list of format names to whitelist. If not given, all formats are allowed. +For advance usages, see [Registries](/docs/registries/). + + + +
+
+ + +`, + "/index.js": ` +const Parchment = Quill.import('parchment'); + +const quill = new Quill('#editor', { + formats: ['italic'], +}); + +const Delta = Quill.import('delta'); +quill.setContents( + new Delta() + .insert('Only ') + .insert('italic', { italic: true }) + .insert(' is allowed. ') + .insert('Bold', { bold: true }) + .insert(' is not.') +); +`}} +/> \ No newline at end of file