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

feat: add support for TrustedHTML #481

Merged
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
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"@floating-ui/core": "^1.2.1",
"@floating-ui/dom": "^1.2.1",
"@types/jasmine": "~4.0.0",
"@types/trusted-types": "~2.0.3",
"codemirror": "^6.0.1",
"eslint": "^8.34.0",
"eslint-config-pegasus": "^3.5.0",
Expand Down
3 changes: 2 additions & 1 deletion projects/ngx-editor/src/lib/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import EditorCommands from './EditorCommands';
import defautlSchema from './schema';
import { parseContent } from './parsers';
import getDefaultPlugins from './defaultPlugins';
import { HTML } from './trustedTypesUtil';

type JSONDoc = Record<string, any>;
type Content = string | null | JSONDoc;
type Content = HTML | null | JSONDoc;

interface Options {
content?: Content;
Expand Down
6 changes: 4 additions & 2 deletions projects/ngx-editor/src/lib/EditorCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import HeadingCommand, { HeadingLevels } from './commands/Heading';
import ImageCommand, { ImageAttrs } from './commands/Image';
import TextColorCommand from './commands/TextColor';
import TextAlignCommand, { Align } from './commands/TextAlign';
import { HTML } from './trustedTypesUtil';
import { isString } from './stringUtil';

const execMark = (name: string, toggle = false) => {
return (state: EditorState, dispatch: (tr: Transaction) => void) => {
Expand Down Expand Up @@ -226,12 +228,12 @@ class EditorCommands {
return this;
}

insertHTML(html: string): this {
insertHTML(html: HTML): this {
const { selection, schema, tr } = this.state;
const { from, to } = selection;

const element = document.createElement('div');
element.innerHTML = html.trim();
element.innerHTML = isString(html) ? (html as string).trim() : html as any;
const slice = DOMParser.fromSchema(schema).parseSlice(element);

const transaction = tr.replaceRange(from, to, slice);
Expand Down
9 changes: 5 additions & 4 deletions projects/ngx-editor/src/lib/editor.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import { takeUntil } from 'rxjs/operators';

import { NgxEditorError } from 'ngx-editor/utils';
import * as plugins from './plugins';
import { toHTML } from './parsers';
import { emptyDoc, toHTML } from './parsers';
import Editor from './Editor';
import { HTML, isHtml } from './trustedTypesUtil';

@Component({
selector: 'ngx-editor',
Expand Down Expand Up @@ -44,12 +45,12 @@ export class NgxEditorComponent implements ControlValueAccessor, OnInit, OnChang
private onChange: (value: Record<string, any> | string) => void = () => { /** */ };
private onTouched: () => void = () => { /** */ };

writeValue(value: Record<string, any> | string | null): void {
if (!this.outputFormat && typeof value === 'string') {
writeValue(value: Record<string, any> | HTML | null): void {
if (!this.outputFormat && isHtml(value)) {
this.outputFormat = 'html';
}

this.editor.setContent(value ?? '');
this.editor.setContent(value ?? emptyDoc);
}

registerOnChange(fn: () => void): void {
Expand Down
3 changes: 2 additions & 1 deletion projects/ngx-editor/src/lib/editor.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { NgxEditorConfig } from './types';
import Locals from './Locals';
import { NgxEditorServiceConfig } from './editor-config.service';
import Icon from './icons';
import { HTML } from './trustedTypesUtil';

@Injectable({
providedIn: 'root',
Expand All @@ -19,7 +20,7 @@ export class NgxEditorService {
return new Locals(this.config.locals);
}

getIcon(icon: string): string {
getIcon(icon: string): HTML {
return this.config.icons[icon] ? this.config.icons[icon] : Icon.get(icon);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Observable, Subscription } from 'rxjs';
import { NgxEditorService } from '../../../editor.service';
import { MenuService } from '../menu.service';
import { TextColor, TextBackgroundColor } from '../MenuCommands';
import { HTML } from '../../../trustedTypesUtil';

type Command = typeof TextColor | typeof TextBackgroundColor;

Expand All @@ -30,7 +31,7 @@ export class ColorPickerComponent implements OnInit, OnDestroy {
return this.getLabel(this.type === 'text_color' ? 'text_color' : 'background_color');
}

get icon(): string {
get icon(): HTML {
return this.ngxeService.getIcon(this.type === 'text_color' ? 'text_color' : 'color_fill');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Observable, Subscription } from 'rxjs';
import { NgxEditorService } from '../../../editor.service';
import { MenuService } from '../menu.service';
import { Image as ImageCommand } from '../MenuCommands';
import { HTML } from '../../../trustedTypesUtil';

@Component({
selector: 'ngx-image',
Expand Down Expand Up @@ -38,7 +39,7 @@ export class ImageComponent implements OnInit, OnDestroy {
private menuService: MenuService,
) { }

get icon(): string {
get icon(): HTML {
return this.ngxeService.getIcon('image');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { InsertCommands } from '../MenuCommands';
import { NgxEditorService } from '../../../editor.service';
import { MenuService } from '../menu.service';
import { TBItems, ToolbarItem } from '../../../types';
import icons from '../../../icons';
import { HTML } from '../../../trustedTypesUtil';

@Component({
selector: 'ngx-insert-command',
Expand All @@ -21,7 +21,7 @@ export class InsertCommandComponent implements OnInit, OnDestroy {
return this.toolbarItem as TBItems;
}

html: string;
html: HTML;
editorView: EditorView;
disabled = false;
private updateSubscription: Subscription;
Expand Down Expand Up @@ -54,7 +54,7 @@ export class InsertCommandComponent implements OnInit, OnDestroy {
}

ngOnInit(): void {
this.html = icons.get(this.name);
this.html = this.ngxeService.getIcon(this.name);

this.editorView = this.menuService.editor.view;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Observable, Subscription } from 'rxjs';
import { NgxEditorService } from '../../../editor.service';
import { MenuService } from '../menu.service';
import { Link as LinkCommand } from '../MenuCommands';
import { HTML } from '../../../trustedTypesUtil';

@Component({
selector: 'ngx-link',
Expand All @@ -31,7 +32,7 @@ export class LinkComponent implements OnInit, OnDestroy {
private menuService: MenuService,
) { }

get icon(): string {
get icon(): HTML {
return this.ngxeService.getIcon(this.isActive ? 'unlink' : 'link');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ToggleCommands } from '../MenuCommands';
import { NgxEditorService } from '../../../editor.service';
import { MenuService } from '../menu.service';
import { TBItems, ToolbarItem } from '../../../types';
import { HTML } from '../../../trustedTypesUtil';

@Component({
selector: 'ngx-toggle-command',
Expand All @@ -20,7 +21,7 @@ export class ToggleCommandComponent implements OnInit, OnDestroy {
return this.toolbarItem as TBItems;
}

html: string;
html: HTML;
editorView: EditorView;
isActive = false;
disabled = false;
Expand Down
11 changes: 6 additions & 5 deletions projects/ngx-editor/src/lib/parsers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DOMSerializer, Schema, DOMParser, Node as ProseMirrorNode } from 'prosemirror-model';

import defaultSchema from './schema';
import { HTML, isHtml } from './trustedTypesUtil';

export const emptyDoc = {
type: 'doc',
Expand All @@ -23,24 +24,24 @@ export const toHTML = (json: Record<string, any>, inputSchema?: Schema): string
return div.innerHTML;
};

export const toDoc = (html: string, inputSchema?: Schema): Record<string, any> => {
export const toDoc = (html: HTML, inputSchema?: Schema): Record<string, any> => {
const schema = inputSchema ?? defaultSchema;

const el = document.createElement('div');
el.innerHTML = html;
el.innerHTML = html as any;

return DOMParser.fromSchema(schema).parse(el).toJSON();
};

export const parseContent = (value: string | Record<string, any> | null, schema: Schema): ProseMirrorNode => {
export const parseContent = (value: HTML | Record<string, any> | null, schema: Schema): ProseMirrorNode => {
if (!value) {
return schema.nodeFromJSON(emptyDoc);
}

if (typeof value !== 'string') {
if (!isHtml(value)) {
return schema.nodeFromJSON(value);
}

const docJson = toDoc(value, schema);
const docJson = toDoc(value as HTML, schema);
return schema.nodeFromJSON(docJson);
};
12 changes: 9 additions & 3 deletions projects/ngx-editor/src/lib/pipes/sanitize/sanitize-html.pipe.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { TrustedHTML } from 'trusted-types/lib';
import { HTML, isTrustedHtml } from '../../trustedTypesUtil';

@Pipe({
name: 'sanitizeHtml',
})

export class SanitizeHtmlPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) { }
constructor(private sanitizer: DomSanitizer) {
}

transform(value: string): SafeHtml {
return this.sanitizer.bypassSecurityTrustHtml(value);
transform(value: HTML): SafeHtml | TrustedHTML {
if (isTrustedHtml(value)) {
return value as TrustedHTML;
}
return this.sanitizer.bypassSecurityTrustHtml(value as string);
}
}
3 changes: 3 additions & 0 deletions projects/ngx-editor/src/lib/stringUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const isString = (value: unknown): boolean => {
return typeof value === 'string';
};
16 changes: 16 additions & 0 deletions projects/ngx-editor/src/lib/trustedTypesUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TrustedTypePolicyFactory, TrustedTypesWindow, TrustedHTML } from 'trusted-types/lib';
import { isString } from './stringUtil';

export const getTrustedTypes = (): TrustedTypePolicyFactory | undefined => {
return (window as unknown as TrustedTypesWindow).trustedTypes;
};

export const isTrustedHtml = (value: unknown): boolean => {
return getTrustedTypes()?.isHTML(value) ?? false;
};

export const isHtml = (value: unknown): boolean => {
return isString(value) || isTrustedHtml(value);
};

export type HTML = string | TrustedHTML;