diff --git a/packages/quill/src/core/editor.ts b/packages/quill/src/core/editor.ts
index b6391a7cd4..7e05f355d8 100644
--- a/packages/quill/src/core/editor.ts
+++ b/packages/quill/src/core/editor.ts
@@ -16,6 +16,14 @@ type SelectionInfo = {
oldRange: Range;
};
+export type SemanticHTMLOptions = {
+ preserveWhitespace?: boolean;
+};
+
+const defaultSemanticHTMLOptions: SemanticHTMLOptions = {
+ preserveWhitespace: false,
+} as const;
+
class Editor {
scroll: Scroll;
delta: Delta;
@@ -195,15 +203,20 @@ class Editor {
return { ...lineFormats, ...leafFormats };
}
- getHTML(index: number, length: number): string {
+ getHTML(
+ index: number,
+ length: number,
+ options?: SemanticHTMLOptions,
+ ): string {
+ const finalOptions = { ...defaultSemanticHTMLOptions, ...options };
const [line, lineOffset] = this.scroll.line(index);
if (line) {
const lineLength = line.length();
const isWithinLine = line.length() >= lineOffset + length;
if (isWithinLine && !(lineOffset === 0 && length === lineLength)) {
- return convertHTML(line, lineOffset, length, true);
+ return convertHTML(line, lineOffset, length, finalOptions, true);
}
- return convertHTML(this.scroll, index, length, true);
+ return convertHTML(this.scroll, index, length, finalOptions, true);
}
return '';
}
@@ -327,13 +340,14 @@ function convertListHTML(
items: ListItem[],
lastIndent: number,
types: string[],
+ options: SemanticHTMLOptions,
): string {
if (items.length === 0) {
const [endTag] = getListType(types.pop());
if (lastIndent <= 0) {
return `${endTag}>`;
}
- return `${endTag}>${convertListHTML([], lastIndent - 1, types)}`;
+ return `${endTag}>${convertListHTML([], lastIndent - 1, types, options)}`;
}
const [{ child, offset, length, indent, type }, ...rest] = items;
const [tag, attribute] = getListType(type);
@@ -344,9 +358,10 @@ function convertListHTML(
child,
offset,
length,
- )}${convertListHTML(rest, indent, types)}`;
+ options,
+ )}${convertListHTML(rest, indent, types, options)}`;
}
- return `<${tag}>
${convertListHTML(items, lastIndent + 1, types)}`;
+ return `<${tag}>${convertListHTML(items, lastIndent + 1, types, options)}`;
}
const previousType = types[types.length - 1];
if (indent === lastIndent && type === previousType) {
@@ -354,16 +369,18 @@ function convertListHTML(
child,
offset,
length,
- )}${convertListHTML(rest, indent, types)}`;
+ options,
+ )}${convertListHTML(rest, indent, types, options)}`;
}
const [endTag] = getListType(types.pop());
- return `${endTag}>${convertListHTML(items, lastIndent - 1, types)}`;
+ return `${endTag}>${convertListHTML(items, lastIndent - 1, types, options)}`;
}
function convertHTML(
blot: Blot,
index: number,
length: number,
+ options: SemanticHTMLOptions,
isRoot = false,
): string {
if ('html' in blot && typeof blot.html === 'function') {
@@ -371,6 +388,7 @@ function convertHTML(
}
if (blot instanceof TextBlot) {
const escapedText = escapeText(blot.value().slice(index, index + length));
+ if (options.preserveWhitespace) return escapedText;
return escapedText.replaceAll(' ', ' ');
}
if (blot instanceof ParentBlot) {
@@ -390,11 +408,11 @@ function convertHTML(
type: formats.list,
});
});
- return convertListHTML(items, -1, []);
+ return convertListHTML(items, -1, [], options);
}
const parts: string[] = [];
blot.children.forEachAt(index, length, (child, offset, childLength) => {
- parts.push(convertHTML(child, offset, childLength));
+ parts.push(convertHTML(child, offset, childLength, options));
});
if (isRoot || blot.statics.blotName === 'list') {
return parts.join('');
diff --git a/packages/quill/src/core/quill.ts b/packages/quill/src/core/quill.ts
index dae5267bcb..6eab5ec54e 100644
--- a/packages/quill/src/core/quill.ts
+++ b/packages/quill/src/core/quill.ts
@@ -9,14 +9,14 @@ import type Clipboard from '../modules/clipboard.js';
import type History from '../modules/history.js';
import type Keyboard from '../modules/keyboard.js';
import type Uploader from '../modules/uploader.js';
-import Editor from './editor.js';
+import Editor, { SemanticHTMLOptions } from './editor.js';
import Emitter from './emitter.js';
import type { EmitterSource } from './emitter.js';
import instances from './instances.js';
import logger from './logger.js';
import type { DebugLevel } from './logger.js';
import Module from './module.js';
-import Selection, { Range } from './selection.js';
+import Selection, { isRange, Range } from './selection.js';
import type { Bounds } from './selection.js';
import Composition from './composition.js';
import Theme from './theme.js';
@@ -537,15 +537,47 @@ class Quill {
return this.selection.getRange()[0];
}
- getSemanticHTML(range: Range): string;
- getSemanticHTML(index?: number, length?: number): string;
- getSemanticHTML(index: Range | number = 0, length?: number) {
- if (typeof index === 'number') {
- length = length ?? this.getLength() - index;
+ getSemanticHTML(options?: SemanticHTMLOptions): string;
+ getSemanticHTML(range: Range, options?: SemanticHTMLOptions): string;
+ getSemanticHTML(index: number, options?: SemanticHTMLOptions): string;
+ getSemanticHTML(
+ index: number,
+ length: number,
+ options?: SemanticHTMLOptions,
+ ): string;
+ getSemanticHTML(
+ indexOrRangeOrOptions?: Range | number | SemanticHTMLOptions,
+ lengthOrOptions?: number | SemanticHTMLOptions,
+ options?: SemanticHTMLOptions,
+ ) {
+ let finalIndex: number | Range = 0;
+ let finalLength: number | undefined = undefined;
+ let finalOptions: SemanticHTMLOptions = {};
+
+ if (indexOrRangeOrOptions === undefined) {
+ finalIndex = 0;
+ finalLength = this.getLength() - finalIndex;
+ } else if (isRange(indexOrRangeOrOptions)) {
+ finalIndex = indexOrRangeOrOptions as Range;
+ finalOptions = (lengthOrOptions as SemanticHTMLOptions) ?? {};
+ } else if (typeof indexOrRangeOrOptions === 'number') {
+ finalIndex = indexOrRangeOrOptions;
+ if (typeof lengthOrOptions === 'number') {
+ finalLength = lengthOrOptions;
+ finalOptions = options ?? {};
+ } else {
+ finalLength = this.getLength() - finalIndex;
+ finalOptions = (lengthOrOptions as SemanticHTMLOptions) ?? {};
+ }
+ } else {
+ finalIndex = 0;
+ finalLength = this.getLength() - finalIndex;
+ finalOptions = indexOrRangeOrOptions;
}
+
// @ts-expect-error
- [index, length] = overload(index, length);
- return this.editor.getHTML(index, length);
+ [finalIndex, finalLength] = overload(finalIndex, finalLength);
+ return this.editor.getHTML(finalIndex, finalLength, finalOptions);
}
getText(range?: Range): string;
diff --git a/packages/quill/src/core/selection.ts b/packages/quill/src/core/selection.ts
index 33f7f2a576..44510ec46b 100644
--- a/packages/quill/src/core/selection.ts
+++ b/packages/quill/src/core/selection.ts
@@ -35,6 +35,12 @@ export class Range {
) {}
}
+export function isRange(value: any): value is Range {
+ return (
+ value && typeof value === 'object' && 'index' in value && 'length' in value
+ );
+}
+
class Selection {
scroll: Scroll;
emitter: Emitter;
diff --git a/packages/quill/test/unit/core/editor.spec.ts b/packages/quill/test/unit/core/editor.spec.ts
index 2d47a1dec9..1f5f44b9ec 100644
--- a/packages/quill/test/unit/core/editor.spec.ts
+++ b/packages/quill/test/unit/core/editor.spec.ts
@@ -1281,6 +1281,7 @@ describe('Editor', () => {
`,
);
+
expect(editor.getHTML(2, 12)).toEqualHTML(`
- e
@@ -1409,5 +1410,17 @@ describe('Editor', () => {
expect(editor.getHTML(2, 7)).toEqual('\n123\n\n\n4\n
');
expect(editor.getHTML(5, 7)).toEqual('\n\n\n\n4567\n
');
});
+
+ test('option preserveWhitespace is disabled (DEFAULT)', () => {
+ const editor = createEditor('This is Quill
');
+ expect(editor.getHTML(0, 14)).toEqual('This is Quill
');
+ });
+
+ test('option preserveWhitespace is enabled', () => {
+ const editor = createEditor('This is Quill
');
+ expect(editor.getHTML(0, 14, { preserveWhitespace: true })).toEqual(
+ 'This is Quill
',
+ );
+ });
});
});
diff --git a/packages/quill/test/unit/core/quill.spec.ts b/packages/quill/test/unit/core/quill.spec.ts
index c128c0dc12..05cf7bcd51 100644
--- a/packages/quill/test/unit/core/quill.spec.ts
+++ b/packages/quill/test/unit/core/quill.spec.ts
@@ -604,9 +604,33 @@ describe('Quill', () => {
test('works with range', () => {
const quill = new Quill(createContainer('Welcome
'));
- expect(quill.getText({ index: 1, length: 2 })).toMatchInlineSnapshot(
- '"el"',
- );
+ expect(
+ quill.getSemanticHTML({ index: 1, length: 2 }),
+ ).toMatchInlineSnapshot('"el"');
+ });
+
+ test('works with only options', () => {
+ const quill = new Quill(createContainer('Welcome to quill
'));
+ expect(quill.getSemanticHTML({ preserveWhitespace: true }))
+ .toMatchInlineSnapshot(`
+ "Welcome to quill
"
+ `);
+ });
+
+ test('works with index and options', () => {
+ const quill = new Quill(createContainer('Welcome to quill
'));
+ expect(quill.getSemanticHTML(0, { preserveWhitespace: true }))
+ .toMatchInlineSnapshot(`
+ "Welcome to quill
"
+ `);
+ });
+
+ test('works with index, length and options', () => {
+ const quill = new Quill(createContainer('Welcome to quill
'));
+ expect(quill.getSemanticHTML(0, 10, { preserveWhitespace: true }))
+ .toMatchInlineSnapshot(`
+ "Welcome to"
+ `);
});
});
diff --git a/packages/website/content/docs/api.mdx b/packages/website/content/docs/api.mdx
index 66d456eb76..100fa64249 100644
--- a/packages/website/content/docs/api.mdx
+++ b/packages/website/content/docs/api.mdx
@@ -116,10 +116,14 @@ This method is useful for exporting the contents of the editor in a format that
The `length` parameter defaults to the length of the remaining document.
+The `options` parameter allows to modify output:
+
+- `preserveWhitespace` - if true all whitespaces are preserved, otherwise all whitespaces are changed to `" "` (default: false)
+
**Methods**
```typescript
-getSemanticHTML(index: number = 0, length: number = remaining): string
+getSemanticHTML(index: number = 0, length: number = remaining, options?: SemanticHTMLOptions): string
```
**Examples**