Skip to content

Commit

Permalink
Merge pull request #1514 from p3ol/fix/sanitize-html-ssr-error
Browse files Browse the repository at this point in the history
🐛 fix(react): allow to use polyfills to parse html
  • Loading branch information
dackmin authored Oct 25, 2024
2 parents b67bccd + fb1aab8 commit b69ca97
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 33 deletions.
11 changes: 10 additions & 1 deletion packages/react/lib/Builder/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export declare interface BuilderProps extends SpecialComponentPropsWithoutRef {
topHistoryButtonsContainer?: string | HTMLElement | DocumentFragment;
topHistoryButtonsEnabled?: boolean;
value?: Array<ElementObject>;
polyfills?: {
DOMParser: typeof DOMParser;
XMLSerializer: typeof XMLSerializer;
};
onChange?(content: Array<ElementObject>): void;
onImageUpload?(event: FormEvent, opts: {
element?: ElementObject;
Expand All @@ -66,6 +70,7 @@ const Builder = forwardRef<BuilderRef, BuilderProps>(({
onImageUpload,
topHistoryButtonsContainer,
bottomHistoryButtonsContainer,
polyfills,
historyEnabled = true,
topHistoryButtonsEnabled = true,
bottomHistoryButtonsEnabled = true,
Expand Down Expand Up @@ -107,7 +112,11 @@ const Builder = forwardRef<BuilderRef, BuilderProps>(({
rootRef: innerRef,
floatingsRef,
editableType,
}), [builder, content, addons, rootBoundary, onImageUpload, editableType]);
polyfills,
}), [
builder, content, addons, rootBoundary, onImageUpload, polyfills,
editableType,
]);

const onAppend = (component: ComponentObject) => {
catalogueRef.current?.close();
Expand Down
34 changes: 23 additions & 11 deletions packages/react/lib/components/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ElementObject } from '@oakjs/core';
import { Button, classNames } from '@junipero/react';

import { sanitizeHTML } from '../../utils';
import { useBuilder } from '../../hooks';

export interface ButtonProps extends ComponentPropsWithoutRef<typeof Button> {
element: ElementObject;
Expand All @@ -11,17 +12,28 @@ export interface ButtonProps extends ComponentPropsWithoutRef<typeof Button> {
const Button_ = ({
element,
className,
}: ButtonProps) => !element.content ? null : (
<Button
className={classNames(
'default !oak-pointer-events-none sanitize-html',
className
)}
dangerouslySetInnerHTML={
{ __html: sanitizeHTML(element.content as string) }
}
/>
);
}: ButtonProps) => {
const { polyfills } = useBuilder();

if (!element.content) {
return null;
}

return (
<Button
className={classNames(
'default !oak-pointer-events-none sanitize-html',
className
)}
dangerouslySetInnerHTML={
{ __html: sanitizeHTML(element.content as string, {
parser: polyfills?.DOMParser,
serializer: polyfills?.XMLSerializer,
}) }
}
/>
);
};

Button_.displayName = 'Button';

Expand Down
28 changes: 20 additions & 8 deletions packages/react/lib/components/Text/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ElementObject } from '@oakjs/core';
import { classNames } from '@junipero/react';

import { sanitizeHTML } from '../../utils';
import { useBuilder } from '../../hooks';

export interface TextComponentProps extends ComponentPropsWithoutRef<'div'> {
element: ElementObject;
Expand All @@ -11,14 +12,25 @@ export interface TextComponentProps extends ComponentPropsWithoutRef<'div'> {
const Text = ({
element,
className,
}: TextComponentProps) => !element.content ? null : (
<div
className={classNames('junipero sanitize-html', className)}
dangerouslySetInnerHTML={
{ __html: sanitizeHTML(element.content as string) }
}
/>
);
}: TextComponentProps) => {
const { polyfills } = useBuilder();

if (!element.content) {
return null;
}

return (
<div
className={classNames('junipero sanitize-html', className)}
dangerouslySetInnerHTML={
{ __html: sanitizeHTML(element.content as string, {
parser: polyfills?.DOMParser,
serializer: polyfills?.XMLSerializer,
}) }
}
/>
);
};

Text.displayName = 'TextComponent';

Expand Down
12 changes: 10 additions & 2 deletions packages/react/lib/components/Title/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import {
} from '@junipero/react';

import { sanitizeHTML } from '../../utils';
import { useBuilder } from '../../hooks';

export interface TitleProps extends SpecialComponentPropsWithoutRef {
element: ElementObject;
}

const Title = ({ element, className }: TitleProps) => {
const { polyfills } = useBuilder();

const Tag = element.headingLevel || 'h1';
const sizes: { [key: string]: string } = {
h1: '!oak-text-4xl',
Expand All @@ -21,7 +24,9 @@ const Title = ({ element, className }: TitleProps) => {
h6: '!oak-text-md',
};

if (!element.content) return null;
if (!element.content) {
return null;
}

return (
<Tag
Expand All @@ -31,7 +36,10 @@ const Title = ({ element, className }: TitleProps) => {
className
)}
dangerouslySetInnerHTML={
{ __html: sanitizeHTML(element.content as string) }
{ __html: sanitizeHTML(element.content as string, {
parser: polyfills?.DOMParser,
serializer: polyfills?.XMLSerializer,
}) }
}
/>
);
Expand Down
4 changes: 4 additions & 0 deletions packages/react/lib/contexts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export declare type BuilderContextValue = {
rootRef?: MutableRefObject<HTMLDivElement>;
floatingsRef?: MutableRefObject<HTMLDivElement>;
editableType?: EditableType;
polyfills?: {
DOMParser: typeof DOMParser;
XMLSerializer: typeof XMLSerializer;
};
};

export const BuilderContext = createContext<BuilderContextValue>({});
Expand Down
3 changes: 2 additions & 1 deletion packages/react/lib/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {

import type { EditableType } from './types';
import {
type BuilderContextValue,
BuilderContext,
EditableFormContext,
ElementContext,
Expand Down Expand Up @@ -153,6 +154,6 @@ export const useRootBuilder = ({
return { builder, ...state };
};

export const useBuilder = () => useContext(BuilderContext);
export const useBuilder = () => useContext<BuilderContextValue>(BuilderContext);
export const useEditableForm = () => useContext(EditableFormContext);
export const useElement = () => useContext(ElementContext);
27 changes: 17 additions & 10 deletions packages/react/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
export const copyToClipboard = (value: string) =>
globalThis.navigator.clipboard.writeText(value);

export const sanitizeHTML = (content: string) => {
export const sanitizeHTML = (content: string, opts?: {
parser?: typeof DOMParser;
serializer?: typeof XMLSerializer;
}) => {
try {
const parsed = new DOMParser().parseFromString(content, 'text/html');
const parsed = new (opts?.parser || DOMParser)()
.parseFromString(content, 'text/html');

// Remove script & style tags
parsed
.querySelectorAll('script, style, iframe, object, video, audio')
.forEach(item => item.parentNode.removeChild(item));
['script', 'style', 'iframe', 'object', 'video', 'audio'].forEach(t => {
Array.from(parsed.getElementsByTagName(t))
.forEach(item => item.parentNode.removeChild(item));
});

// Disable all links
parsed.querySelectorAll('a[href]').forEach(item => {
item.removeAttribute('href');
item.removeAttribute('target');
item.setAttribute('role', 'link');
Array.from(parsed.getElementsByTagName('a')).forEach(item => {
if (item.hasAttribute('href')) {
item.removeAttribute('href');
item.removeAttribute('target');
item.setAttribute('role', 'link');
}
});

return parsed.body.innerHTML;
return new (opts?.serializer || XMLSerializer)().serializeToString(parsed);
} catch (e) {
console.error(e);

Expand Down

0 comments on commit b69ca97

Please sign in to comment.