Skip to content

Commit

Permalink
Add formats option for specifying allowed formats
Browse files Browse the repository at this point in the history
  • Loading branch information
luin committed Mar 15, 2024
1 parent db7455a commit 17e8d67
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 2 deletions.
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
22 changes: 21 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,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(
Expand Down
40 changes: 40 additions & 0 deletions packages/quill/src/core/utils/createRegistryWithFormats.ts
Original file line number Diff line number Diff line change
@@ -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;
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,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'));
});
});
38 changes: 38 additions & 0 deletions packages/website/content/docs/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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/).

<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.')
);
`}}
/>

0 comments on commit 17e8d67

Please sign in to comment.