Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add formats option for specifying allowed formats #4053

Merged
merged 2 commits into from
Mar 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 15 additions & 1 deletion packages/quill/src/core/quill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -37,9 +38,10 @@ interface Options {
placeholder?: string;
bounds?: HTMLElement | string | null;
modules?: Record<string, unknown>;
formats?: string[] | null;
}

interface ExpandedOptions extends Omit<Options, 'theme'> {
interface ExpandedOptions extends Omit<Options, 'theme' | 'formats'> {
theme: ThemeConstructor;
registry: Parchment.Registry;
container: HTMLElement;
Expand Down Expand Up @@ -785,8 +787,20 @@ 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, debug)
: config.registry;
}

return {
...config,
registry,
container,
theme,
modules: Object.entries(modules).reduce(
Expand Down
42 changes: 42 additions & 0 deletions packages/quill/src/core/utils/createRegistryWithFormats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Registry } from 'parchment';

const MAX_REGISTER_ITERATIONS = 100;
const CORE_FORMATS = ['block', 'break', 'cursor', 'inline', 'scroll', 'text'];

const createRegistryWithFormats = (
formats: string[],
sourceRegistry: Registry,
debug: { error: (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) {
debug.error(
`Cannot register "${name}" specified in "formats" config. Are you sure it was registered?`,
);
}
let iterations = 0;
while (format) {
registry.register(format);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should suppress the warning in case two formats have the same requiredContainer

format = 'blotName' in format ? format.requiredContainer ?? null : null;

iterations += 1;
if (iterations > MAX_REGISTER_ITERATIONS) {
debug.error(
`Cycle detected in registering blot requiredContainer: "${name}"`,
);
break;
}
}
});

return registry;
};

export default createRegistryWithFormats;
75 changes: 74 additions & 1 deletion packages/quill/test/unit/core/quill.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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';
import logger from '../../../../src/core/logger.js';
import { Registry } from 'parchment';
import Inline from '../../../../src/blots/inline.js';
import Container from '../../../../src/blots/container.js';

const debug = logger('test');

describe('createRegistryWithFormats', () => {
test('register core formats', () => {
const registry = createRegistryWithFormats([], globalRegistry, debug);
expect(registry.query('cursor')).toBeTruthy();
expect(registry.query('bold')).toBeFalsy();
});

test('register specified formats', () => {
const registry = createRegistryWithFormats(['bold'], globalRegistry, debug);
expect(registry.query('cursor')).toBeTruthy();
expect(registry.query('bold')).toBeTruthy();
});

test('register required container', () => {
const sourceRegistry = new Registry();

class RequiredContainer extends Container {
static blotName = 'my-required-container';
}
class Child extends Inline {
static requiredContainer = RequiredContainer;
static blotName = 'my-child';
}

sourceRegistry.register(Child);

const registry = createRegistryWithFormats(
['my-child'],
sourceRegistry,
debug,
);

expect(registry.query('my-child')).toBeTruthy();
expect(registry.query('my-required-container')).toBeTruthy();
});

test('infinite loop', () => {
const sourceRegistry = new Registry();

class InfiniteBlot extends Inline {
static requiredContainer = InfiniteBlot;
static blotName = 'infinite-blot';
}

sourceRegistry.register(InfiniteBlot);

const logError = vitest.spyOn(debug, 'error');
const registry = createRegistryWithFormats(
['infinite-blot'],
sourceRegistry,
debug,
);

expect(registry.query('infinite-blot')).toBeTruthy();
expect(logError).toHaveBeenCalledWith(
expect.stringMatching('Cycle detected'),
);
});

test('report missing formats', () => {
const logError = vitest.spyOn(debug, 'error');
const registry = createRegistryWithFormats(
['my-unknown'],
globalRegistry,
debug,
);
expect(registry.query('my-unknown')).toBeFalsy();
expect(logError).toHaveBeenCalledWith(expect.stringMatching('my-unknown'));
});
});
48 changes: 48 additions & 0 deletions packages/website/content/docs/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,49 @@ Default: `warn`

Shortcut for [debug](/docs/api/#debug). Note `debug` is a static method and will affect other instances of Quill editors on the page. Only warning and error messages are enabled by default.

### formats

Default: `null`

A list of formats that are recognized and can exist within the editor contents.

By default, all formats that are defined in the Quill library are allowed.
To restrict formatting to a smaller list, pass in an array of the format names to support.

You can create brand new formats or more fully customize the content using [Registries](/docs/registries/).
Specifying a `registry` option will ignore this `formats` option.

<Sandpack
defaultShowPreview
activeFile="index.js"
files={{
'index.html': `
<!-- Include stylesheet -->
<link href="{{site.cdn}}/quill.snow.css" rel="stylesheet" />
<div id="editor">
</div>
<!-- Include the Quill library -->
<script src="{{site.cdn}}/quill.js"></script>
<script src="/index.js"></script>`,
"/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.')
);
`}}
/>

### placeholder

Default: None
Expand Down Expand Up @@ -100,6 +143,11 @@ quill.setContents(
`}}
/>

### registry

Default: `null`

By default all formats defined by Quill are supported in the editor contents through a shared registry between editor instances. Use `formats` to restrict formatting for simple use cases and `registry` for greater customization. Specifying this `registry` option will ignore the `formatting` option. Learn more about [Registries](/docs/registries/).

### theme

Expand Down
Loading