Skip to content

Add getSemanticHTML options & Add preserveWhitespace option to getSem… #4598

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
38 changes: 28 additions & 10 deletions packages/quill/src/core/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 '';
}
Expand Down Expand Up @@ -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 `</li></${endTag}>`;
}
return `</li></${endTag}>${convertListHTML([], lastIndent - 1, types)}`;
return `</li></${endTag}>${convertListHTML([], lastIndent - 1, types, options)}`;
}
const [{ child, offset, length, indent, type }, ...rest] = items;
const [tag, attribute] = getListType(type);
Expand All @@ -344,33 +358,37 @@ function convertListHTML(
child,
offset,
length,
)}${convertListHTML(rest, indent, types)}`;
options,
)}${convertListHTML(rest, indent, types, options)}`;
}
return `<${tag}><li>${convertListHTML(items, lastIndent + 1, types)}`;
return `<${tag}><li>${convertListHTML(items, lastIndent + 1, types, options)}`;
}
const previousType = types[types.length - 1];
if (indent === lastIndent && type === previousType) {
return `</li><li${attribute}>${convertHTML(
child,
offset,
length,
)}${convertListHTML(rest, indent, types)}`;
options,
)}${convertListHTML(rest, indent, types, options)}`;
}
const [endTag] = getListType(types.pop());
return `</li></${endTag}>${convertListHTML(items, lastIndent - 1, types)}`;
return `</li></${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') {
return blot.html(index, length);
}
if (blot instanceof TextBlot) {
const escapedText = escapeText(blot.value().slice(index, index + length));
if (options.preserveWhitespace) return escapedText;
return escapedText.replaceAll(' ', '&nbsp;');
}
if (blot instanceof ParentBlot) {
Expand All @@ -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('');
Expand Down
50 changes: 41 additions & 9 deletions packages/quill/src/core/quill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions packages/quill/src/core/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 13 additions & 0 deletions packages/quill/test/unit/core/editor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1281,6 +1281,7 @@ describe('Editor', () => {
</ol>
`,
);

expect(editor.getHTML(2, 12)).toEqualHTML(`
<ol>
<li>e</li>
Expand Down Expand Up @@ -1409,5 +1410,17 @@ describe('Editor', () => {
expect(editor.getHTML(2, 7)).toEqual('<pre>\n123\n\n\n4\n</pre>');
expect(editor.getHTML(5, 7)).toEqual('<pre>\n\n\n\n4567\n</pre>');
});

test('option preserveWhitespace is disabled (DEFAULT)', () => {
const editor = createEditor('<p>This is Quill</p>');
expect(editor.getHTML(0, 14)).toEqual('<p>This&nbsp;is&nbsp;Quill</p>');
});

test('option preserveWhitespace is enabled', () => {
const editor = createEditor('<p>This is Quill</p>');
expect(editor.getHTML(0, 14, { preserveWhitespace: true })).toEqual(
'<p>This is Quill</p>',
);
});
});
});
30 changes: 27 additions & 3 deletions packages/quill/test/unit/core/quill.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -604,9 +604,33 @@ describe('Quill', () => {

test('works with range', () => {
const quill = new Quill(createContainer('<h1>Welcome</h1>'));
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('<h1>Welcome to quill</h1>'));
expect(quill.getSemanticHTML({ preserveWhitespace: true }))
.toMatchInlineSnapshot(`
"<h1>Welcome to quill</h1>"
`);
});

test('works with index and options', () => {
const quill = new Quill(createContainer('<h1>Welcome to quill</h1>'));
expect(quill.getSemanticHTML(0, { preserveWhitespace: true }))
.toMatchInlineSnapshot(`
"<h1>Welcome to quill</h1>"
`);
});

test('works with index, length and options', () => {
const quill = new Quill(createContainer('<h1>Welcome to quill</h1>'));
expect(quill.getSemanticHTML(0, 10, { preserveWhitespace: true }))
.toMatchInlineSnapshot(`
"Welcome to"
`);
});
});

Expand Down
6 changes: 5 additions & 1 deletion packages/website/content/docs/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 `"&nbsp;"` (default: false)

**Methods**

```typescript
getSemanticHTML(index: number = 0, length: number = remaining): string
getSemanticHTML(index: number = 0, length: number = remaining, options?: SemanticHTMLOptions): string
```

**Examples**
Expand Down