diff --git a/apps/www/src/app/(app)/dev/page.tsx b/apps/www/src/app/(app)/dev/page.tsx index e4c7e9c7ae..154f8102ef 100644 --- a/apps/www/src/app/(app)/dev/page.tsx +++ b/apps/www/src/app/(app)/dev/page.tsx @@ -39,7 +39,13 @@ import { BaseIndentListPlugin } from '@udecode/plate-indent-list'; import { BaseKbdPlugin } from '@udecode/plate-kbd'; import { BaseLineHeightPlugin } from '@udecode/plate-line-height'; import { BaseLinkPlugin } from '@udecode/plate-link'; -import { BaseImagePlugin, BaseMediaEmbedPlugin } from '@udecode/plate-media'; +import { + BaseAudioPlugin, + BaseFilePlugin, + BaseImagePlugin, + BaseMediaEmbedPlugin, + BaseVideoPlugin, +} from '@udecode/plate-media'; import { BaseTableCellHeaderPlugin, BaseTableCellPlugin, @@ -58,6 +64,7 @@ import { indentValue } from '@/registry/default/example/values/indent-value'; import { kbdValue } from '@/registry/default/example/values/kbd-value'; import { lineHeightValue } from '@/registry/default/example/values/line-height-value'; import { linkValue } from '@/registry/default/example/values/link-value'; +import { mediaValue } from '@/registry/default/example/values/media-value'; import { tableValue } from '@/registry/default/example/values/table-value'; import { BlockquoteStaticElement } from '@/registry/default/plate-static-ui/blockquote-element'; import { CodeBlockElementStatic } from '@/registry/default/plate-static-ui/code-block-element'; @@ -66,12 +73,16 @@ import { CodeLineStaticElement } from '@/registry/default/plate-static-ui/code-l import { CodeSyntaxStaticLeaf } from '@/registry/default/plate-static-ui/code-syntax-leaf'; import { HeadingStaticElement } from '@/registry/default/plate-static-ui/heading-element'; import { HrStaticElement } from '@/registry/default/plate-static-ui/hr-element'; +import { ImageStaticElement } from '@/registry/default/plate-static-ui/image-element'; import { TodoLi, TodoMarker, } from '@/registry/default/plate-static-ui/indent-todo-marker'; import { KbdStaticLeaf } from '@/registry/default/plate-static-ui/kbd-leaf'; import { LinkStaticElement } from '@/registry/default/plate-static-ui/link-element'; +import { MediaAudioStaticElement } from '@/registry/default/plate-static-ui/media-audio-element'; +import { MediaFileStaticElement } from '@/registry/default/plate-static-ui/media-file-element'; +import { MediaVideoStaticElement } from '@/registry/default/plate-static-ui/media-video-element'; import { ParagraphStaticElement } from '@/registry/default/plate-static-ui/paragraph-element'; import { TableCellHeaderStaticElement, @@ -82,13 +93,16 @@ import { TableRowStaticElement } from '@/registry/default/plate-static-ui/table- export default async function DevPage() { const staticComponents = { + [BaseAudioPlugin.key]: MediaAudioStaticElement, [BaseBlockquotePlugin.key]: BlockquoteStaticElement, [BaseBoldPlugin.key]: withProps(PlateStaticLeaf, { as: 'strong' }), [BaseCodeBlockPlugin.key]: CodeBlockElementStatic, [BaseCodeLinePlugin.key]: CodeLineStaticElement, [BaseCodePlugin.key]: CodeStaticLeaf, [BaseCodeSyntaxPlugin.key]: CodeSyntaxStaticLeaf, + [BaseFilePlugin.key]: MediaFileStaticElement, [BaseHorizontalRulePlugin.key]: HrStaticElement, + [BaseImagePlugin.key]: ImageStaticElement, [BaseItalicPlugin.key]: withProps(PlateStaticLeaf, { as: 'em' }), [BaseKbdPlugin.key]: KbdStaticLeaf, [BaseLinkPlugin.key]: LinkStaticElement, @@ -101,6 +115,7 @@ export default async function DevPage() { [BaseTablePlugin.key]: TableStaticElement, [BaseTableRowPlugin.key]: TableRowStaticElement, [BaseUnderlinePlugin.key]: withProps(PlateStaticLeaf, { as: 'u' }), + [BaseVideoPlugin.key]: MediaVideoStaticElement, [HEADING_KEYS.h1]: withProps(HeadingStaticElement, { variant: 'h1' }), [HEADING_KEYS.h2]: withProps(HeadingStaticElement, { variant: 'h2' }), [HEADING_KEYS.h3]: withProps(HeadingStaticElement, { variant: 'h3' }), @@ -111,8 +126,11 @@ export default async function DevPage() { const editorStatic = createSlateEditor({ plugins: [ + BaseVideoPlugin, + BaseAudioPlugin, BaseParagraphPlugin, BaseHeadingPlugin, + BaseMediaEmbedPlugin, BaseBoldPlugin, BaseCodePlugin, BaseItalicPlugin, @@ -177,6 +195,8 @@ export default async function DevPage() { }), BaseLineHeightPlugin, BaseHighlightPlugin, + BaseFilePlugin, + BaseImagePlugin, ], value: [ ...basicNodesValue, @@ -190,6 +210,8 @@ export default async function DevPage() { ...lineHeightValue, ...indentValue, ...indentListValue, + ...mediaValue, + ...alignValue, ], }); diff --git a/apps/www/src/registry/default/plate-static-ui/image-element.tsx b/apps/www/src/registry/default/plate-static-ui/image-element.tsx new file mode 100644 index 0000000000..cc74710964 --- /dev/null +++ b/apps/www/src/registry/default/plate-static-ui/image-element.tsx @@ -0,0 +1,49 @@ +import React from 'react'; + +import type { StaticElementProps } from '@udecode/plate-common'; +import type { TImageElement } from '@udecode/plate-media'; + +import { cn } from '@udecode/cn'; +import { PlateStaticElement } from '@udecode/plate-common'; + +export function ImageStaticElement({ + children, + className, + element, + nodeProps, + ...props +}: StaticElementProps) { + const { + align = 'center', + url, + width, + } = element as TImageElement & { + width: number; + }; + + return ( + +
+
+ +
+
+ {children} +
+ ); +} diff --git a/apps/www/src/registry/default/plate-static-ui/media-audio-element.tsx b/apps/www/src/registry/default/plate-static-ui/media-audio-element.tsx new file mode 100644 index 0000000000..82bdef3d8e --- /dev/null +++ b/apps/www/src/registry/default/plate-static-ui/media-audio-element.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +import type { StaticElementProps } from '@udecode/plate-common'; +import type { TAudioElement } from '@udecode/plate-media'; + +import { cn } from '@udecode/cn'; +import { PlateStaticElement } from '@udecode/plate-common'; + +export function MediaAudioStaticElement({ + children, + className, + element, + ...props +}: StaticElementProps) { + const { url } = element as TAudioElement; + + return ( + +
+
+
+
+ {children} +
+ ); +} diff --git a/apps/www/src/registry/default/plate-static-ui/media-file-element.tsx b/apps/www/src/registry/default/plate-static-ui/media-file-element.tsx new file mode 100644 index 0000000000..77dfe612ce --- /dev/null +++ b/apps/www/src/registry/default/plate-static-ui/media-file-element.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import type { StaticElementProps } from '@udecode/plate-common'; +import type { TFileElement } from '@udecode/plate-media'; + +import { cn } from '@udecode/cn'; +import { PlateStaticElement } from '@udecode/plate-common'; +import { FileUp } from 'lucide-react'; + +export const MediaFileStaticElement = ({ + children, + className, + element, + ...props +}: StaticElementProps) => { + const { name } = element as TFileElement; + + return ( + +
+
+ +
{name}
+
+
+ {children} +
+ ); +}; diff --git a/apps/www/src/registry/default/plate-static-ui/media-video-element.tsx b/apps/www/src/registry/default/plate-static-ui/media-video-element.tsx new file mode 100644 index 0000000000..90c2897852 --- /dev/null +++ b/apps/www/src/registry/default/plate-static-ui/media-video-element.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +import type { StaticElementProps } from '@udecode/plate-common'; +import type { TVideoElement } from '@udecode/plate-media'; + +import { cn } from '@udecode/cn'; +import { PlateStaticElement } from '@udecode/plate-common'; + +export function MediaVideoStaticElement({ + children, + className, + element, + ...props +}: StaticElementProps) { + const { align, url, width } = element as TVideoElement & { + width: number; + }; + + console.log('🚀 ~ align:', align); + + return ( + +
+ +
+ {children} +
+ ); +} diff --git a/packages/core/src/lib/static/__tests__/create-static-editor.ts b/packages/core/src/lib/static/__tests__/create-static-editor.ts new file mode 100644 index 0000000000..100a9251d0 --- /dev/null +++ b/packages/core/src/lib/static/__tests__/create-static-editor.ts @@ -0,0 +1,170 @@ +import { withProps } from '@udecode/cn'; +import { BaseAlignPlugin } from '@udecode/plate-alignment'; +import { + BaseBoldPlugin, + BaseCodePlugin, + BaseStrikethroughPlugin, +} from '@udecode/plate-basic-marks'; +import { BaseItalicPlugin } from '@udecode/plate-basic-marks'; +import { + BaseSubscriptPlugin, + BaseSuperscriptPlugin, + BaseUnderlinePlugin, +} from '@udecode/plate-basic-marks'; +import { BaseBlockquotePlugin } from '@udecode/plate-block-quote'; +import { + BaseCodeBlockPlugin, + BaseCodeLinePlugin, + BaseCodeSyntaxPlugin, +} from '@udecode/plate-code-block'; +import { type Value, PlateStaticLeaf } from '@udecode/plate-common'; +import { + BaseFontBackgroundColorPlugin, + BaseFontColorPlugin, + BaseFontSizePlugin, +} from '@udecode/plate-font'; +import { + BaseHeadingPlugin, + HEADING_KEYS, + HEADING_LEVELS, +} from '@udecode/plate-heading'; +import { BaseHighlightPlugin } from '@udecode/plate-highlight'; +import { BaseHorizontalRulePlugin } from '@udecode/plate-horizontal-rule'; +import { BaseIndentPlugin } from '@udecode/plate-indent'; +import { BaseIndentListPlugin } from '@udecode/plate-indent-list'; +import { BaseKbdPlugin } from '@udecode/plate-kbd'; +import { BaseLineHeightPlugin } from '@udecode/plate-line-height'; +import { BaseLinkPlugin } from '@udecode/plate-link'; +import { BaseImagePlugin, BaseMediaEmbedPlugin } from '@udecode/plate-media'; +import { + BaseTableCellHeaderPlugin, + BaseTableCellPlugin, + BaseTablePlugin, + BaseTableRowPlugin, +} from '@udecode/plate-table'; +import { BaseTogglePlugin } from '@udecode/plate-toggle'; +import { BlockquoteStaticElement } from 'www/src/registry/default/plate-static-ui/blockquote-element'; +import { CodeBlockElementStatic } from 'www/src/registry/default/plate-static-ui/code-block-element'; +import { CodeStaticLeaf } from 'www/src/registry/default/plate-static-ui/code-leaf'; +import { CodeLineStaticElement } from 'www/src/registry/default/plate-static-ui/code-line-element'; +import { CodeSyntaxStaticLeaf } from 'www/src/registry/default/plate-static-ui/code-syntax-leaf'; +import { HeadingStaticElement } from 'www/src/registry/default/plate-static-ui/heading-element'; +import { HrStaticElement } from 'www/src/registry/default/plate-static-ui/hr-element'; +import { + TodoLi, + TodoMarker, +} from 'www/src/registry/default/plate-static-ui/indent-todo-marker'; +import { KbdStaticLeaf } from 'www/src/registry/default/plate-static-ui/kbd-leaf'; +import { LinkStaticElement } from 'www/src/registry/default/plate-static-ui/link-element'; +import { ParagraphStaticElement } from 'www/src/registry/default/plate-static-ui/paragraph-element'; +import { + TableCellHeaderStaticElement, + TableCellStaticElement, +} from 'www/src/registry/default/plate-static-ui/table-cell-element'; +import { TableStaticElement } from 'www/src/registry/default/plate-static-ui/table-element'; +import { TableRowStaticElement } from 'www/src/registry/default/plate-static-ui/table-row-element'; + +import { BaseParagraphPlugin } from '../..'; +import { createSlateEditor } from '../../editor'; + +export const createStaticEditor = (value: Value) => { + return createSlateEditor({ + plugins: [ + BaseParagraphPlugin, + BaseHeadingPlugin, + BaseBoldPlugin, + BaseCodePlugin, + BaseItalicPlugin, + BaseStrikethroughPlugin, + BaseSubscriptPlugin, + BaseSuperscriptPlugin, + BaseUnderlinePlugin, + BaseBlockquotePlugin, + BaseCodeBlockPlugin, + BaseIndentPlugin.extend({ + inject: { + targetPlugins: [ + BaseParagraphPlugin.key, + BaseBlockquotePlugin.key, + BaseCodeBlockPlugin.key, + ], + }, + }), + BaseIndentListPlugin.extend({ + inject: { + targetPlugins: [ + BaseParagraphPlugin.key, + ...HEADING_LEVELS, + BaseBlockquotePlugin.key, + BaseCodeBlockPlugin.key, + BaseTogglePlugin.key, + ], + }, + options: { + listStyleTypes: { + // fire: { + // liComponent: FireLiComponent, + // markerComponent: FireMarker, + // type: 'fire', + // }, + todo: { + liComponent: TodoLi, + markerComponent: TodoMarker, + type: 'todo', + }, + }, + }, + }), + BaseLinkPlugin, + BaseTableRowPlugin, + BaseTablePlugin, + BaseTableCellPlugin, + BaseHorizontalRulePlugin, + BaseFontColorPlugin, + BaseFontBackgroundColorPlugin, + BaseFontSizePlugin, + BaseKbdPlugin, + BaseAlignPlugin.extend({ + inject: { + targetPlugins: [ + BaseParagraphPlugin.key, + BaseMediaEmbedPlugin.key, + ...HEADING_LEVELS, + BaseImagePlugin.key, + ], + }, + }), + BaseLineHeightPlugin, + BaseHighlightPlugin, + ], + value, + }); +}; + +export const staticComponents = { + [BaseBlockquotePlugin.key]: BlockquoteStaticElement, + [BaseBoldPlugin.key]: withProps(PlateStaticLeaf, { as: 'strong' }), + [BaseCodeBlockPlugin.key]: CodeBlockElementStatic, + [BaseCodeLinePlugin.key]: CodeLineStaticElement, + [BaseCodePlugin.key]: CodeStaticLeaf, + [BaseCodeSyntaxPlugin.key]: CodeSyntaxStaticLeaf, + [BaseHorizontalRulePlugin.key]: HrStaticElement, + [BaseItalicPlugin.key]: withProps(PlateStaticLeaf, { as: 'em' }), + [BaseKbdPlugin.key]: KbdStaticLeaf, + [BaseLinkPlugin.key]: LinkStaticElement, + [BaseParagraphPlugin.key]: ParagraphStaticElement, + [BaseStrikethroughPlugin.key]: withProps(PlateStaticLeaf, { as: 'del' }), + [BaseSubscriptPlugin.key]: withProps(PlateStaticLeaf, { as: 'sub' }), + [BaseSuperscriptPlugin.key]: withProps(PlateStaticLeaf, { as: 'sup' }), + [BaseTableCellHeaderPlugin.key]: TableCellHeaderStaticElement, + [BaseTableCellPlugin.key]: TableCellStaticElement, + [BaseTablePlugin.key]: TableStaticElement, + [BaseTableRowPlugin.key]: TableRowStaticElement, + [BaseUnderlinePlugin.key]: withProps(PlateStaticLeaf, { as: 'u' }), + [HEADING_KEYS.h1]: withProps(HeadingStaticElement, { variant: 'h1' }), + [HEADING_KEYS.h2]: withProps(HeadingStaticElement, { variant: 'h2' }), + [HEADING_KEYS.h3]: withProps(HeadingStaticElement, { variant: 'h3' }), + [HEADING_KEYS.h4]: withProps(HeadingStaticElement, { variant: 'h4' }), + [HEADING_KEYS.h5]: withProps(HeadingStaticElement, { variant: 'h5' }), + [HEADING_KEYS.h6]: withProps(HeadingStaticElement, { variant: 'h6' }), +}; diff --git a/packages/core/src/lib/static/__tests__/element.spec.ts b/packages/core/src/lib/static/__tests__/element.spec.ts new file mode 100644 index 0000000000..bb15f6f05b --- /dev/null +++ b/packages/core/src/lib/static/__tests__/element.spec.ts @@ -0,0 +1,105 @@ +import { decode } from 'html-entities'; + +import { serializePlateStatic } from '../serializedHtml'; +import { createStaticEditor, staticComponents } from './create-static-editor'; + +describe('serializePlateStatic', () => { + it('should serialize paragraph to html', async () => { + const editor = createStaticEditor([ + { + children: [{ text: 'Some random paragraph here...' }], + type: 'p', + }, + ]); + + const html = await serializePlateStatic(editor, staticComponents); + expect(html).toContain('Some random paragraph here...'); + }); + + it('should serialize headings to html', async () => { + const editor = createStaticEditor([ + { + children: [{ text: 'Heading 1' }], + type: 'h1', + }, + { + children: [{ text: 'Heading 2' }], + type: 'h2', + }, + { + children: [{ text: 'Heading 3' }], + type: 'h3', + }, + ]); + + const html = await serializePlateStatic(editor, staticComponents); + expect(html).toContain('Heading 1'); + expect(html).toContain('Heading 2'); + expect(html).toContain('Heading 3'); + }); + + it('should serialize blockquote to html', async () => { + const editor = createStaticEditor([ + { + children: [{ text: 'Blockquoted text here...' }], + type: 'blockquote', + }, + ]); + + const html = await serializePlateStatic(editor, staticComponents); + expect(html).toContain('Blockquoted text here...'); + }); + + it('should serialize link to html', async () => { + const editor = createStaticEditor([ + { children: [{ text: 'Some paragraph of text with ' }], type: 'p' }, + { + children: [{ text: 'link' }], + type: 'a', + url: 'https://example.com/', + }, + { children: [{ text: ' part.' }], type: 'p' }, + ]); + + const html = await serializePlateStatic(editor, {}); + expect(html).toContain(decode('href="https://example.com/"')); + expect(html).toContain('slate-a'); + }); + + // it('should serialize image to html', async () => { + // const editor = createSlateEditor({ + // plugins: [BaseImagePlugin], + // value: [ + // { + // children: [{ text: '' }], + // type: 'img', + // url: 'https://example.com/image.jpg', + // }, + // ], + // }); + + // const html = await serializePlateStatic(editor, {}); + // expect(html).toContain('src="https://example.com/image.jpg"'); + // }); + + it('should serialize table to html', async () => { + const editor = createStaticEditor([ + { + children: [ + { + children: [ + { children: [{ text: 'Cell 1' }], type: 'td' }, + { children: [{ text: 'Cell 2' }], type: 'td' }, + ], + type: 'tr', + }, + ], + type: 'table', + }, + ]); + + const html = await serializePlateStatic(editor, {}); + expect(html).toContain('Cell 1'); + expect(html).toContain('Cell 2'); + }); +}); diff --git a/packages/core/src/lib/static/__tests__/mark.spec.ts b/packages/core/src/lib/static/__tests__/mark.spec.ts new file mode 100644 index 0000000000..aebd1f8fa0 --- /dev/null +++ b/packages/core/src/lib/static/__tests__/mark.spec.ts @@ -0,0 +1,186 @@ +import { serializePlateStatic } from '../serializedHtml'; +import { createStaticEditor, staticComponents } from './create-static-editor'; + +describe('serializePlateStatic marks', () => { + it('should serialize bold to html', async () => { + const editor = createStaticEditor([ + { + children: [ + { text: 'Some paragraph of text with ' }, + { bold: true, text: 'bold' }, + { text: ' part.' }, + ], + type: 'p', + }, + ]); + + const html = await serializePlateStatic(editor, staticComponents, { + preserveClassNames: [], + stripClassNames: true, + stripDataAttributes: true, + }); + expect(html).toContain('bold'); + }); + + it('should serialize italic to html', async () => { + const editor = createStaticEditor([ + { + children: [ + { text: 'Some paragraph of text with ' }, + { italic: true, text: 'italic' }, + { text: ' part.' }, + ], + type: 'p', + }, + ]); + + const html = await serializePlateStatic(editor, staticComponents, { + preserveClassNames: [], + stripClassNames: true, + stripDataAttributes: true, + }); + expect(html).toContain('italic'); + }); + + it('should serialize underline to html', async () => { + const editor = createStaticEditor([ + { + children: [ + { text: 'Some paragraph of text with ' }, + { text: 'underlined', underline: true }, + { text: ' part.' }, + ], + type: 'p', + }, + ]); + + const html = await serializePlateStatic(editor, staticComponents, { + preserveClassNames: [], + stripClassNames: true, + stripDataAttributes: true, + }); + expect(html).toContain('underlined'); + }); + + it('should serialize strikethrough to html', async () => { + const editor = createStaticEditor([ + { + children: [ + { text: 'Some paragraph of text with ' }, + { strikethrough: true, text: 'strikethrough' }, + { text: ' part.' }, + ], + type: 'p', + }, + ]); + + const html = await serializePlateStatic(editor, staticComponents, { + preserveClassNames: [], + stripClassNames: true, + stripDataAttributes: true, + }); + expect(html).toContain('strikethrough'); + }); + + it('should serialize code to html', async () => { + const editor = createStaticEditor([ + { + children: [ + { text: 'Some paragraph of text with ' }, + { code: true, text: 'some code' }, + { text: ' part.' }, + ], + type: 'p', + }, + ]); + + const html = await serializePlateStatic(editor, staticComponents, { + preserveClassNames: [], + stripClassNames: true, + stripDataAttributes: true, + }); + expect(html).toContain('some code'); + }); + + it('should serialize subscript to html', async () => { + const editor = createStaticEditor([ + { + children: [ + { text: 'Some paragraph of text with ' }, + { subscript: true, text: 'subscripted' }, + { text: ' part.' }, + ], + type: 'p', + }, + ]); + + const html = await serializePlateStatic(editor, staticComponents, { + preserveClassNames: [], + stripClassNames: true, + stripDataAttributes: true, + }); + expect(html).toContain('subscripted'); + }); + + it('should serialize superscript to html', async () => { + const editor = createStaticEditor([ + { + children: [ + { text: 'Some paragraph of text with ' }, + { superscript: true, text: 'superscripted' }, + { text: ' part.' }, + ], + type: 'p', + }, + ]); + + const html = await serializePlateStatic(editor, staticComponents, { + preserveClassNames: [], + stripClassNames: true, + stripDataAttributes: true, + }); + expect(html).toContain('superscripted'); + }); + + it('should serialize kbd to html', async () => { + const editor = createStaticEditor([ + { + children: [ + { text: 'Some paragraph of text with ' }, + { kbd: true, text: 'keyboard shortcut' }, + { text: ' part.' }, + ], + type: 'p', + }, + ]); + + const html = await serializePlateStatic(editor, staticComponents, { + preserveClassNames: [], + stripClassNames: true, + stripDataAttributes: true, + }); + expect(html).toContain('keyboard shortcut'); + }); + + it('should serialize multiple marks together to html', async () => { + const editor = createStaticEditor([ + { + children: [ + { text: 'Some paragraph of text with ' }, + { bold: true, italic: true, text: 'bold and italic' }, + { text: ' part.' }, + ], + type: 'p', + }, + ]); + + const html = await serializePlateStatic(editor, staticComponents, { + preserveClassNames: [], + stripClassNames: true, + stripDataAttributes: true, + }); + expect(html).toContain( + 'bold and italic' + ); + }); +}); diff --git a/packages/core/src/lib/static/pipeRenderStaticElement.tsx b/packages/core/src/lib/static/pipeRenderStaticElement.tsx index 8360fdbd74..c60184e2e1 100644 --- a/packages/core/src/lib/static/pipeRenderStaticElement.tsx +++ b/packages/core/src/lib/static/pipeRenderStaticElement.tsx @@ -1,5 +1,8 @@ import React from 'react'; +import type { Path } from 'slate'; + +import { findNode } from '@udecode/slate'; import clsx from 'clsx'; import type { SlateEditor } from '../editor'; @@ -30,7 +33,11 @@ export const getRenderStaticNodeProps = ({ className: clsx(getSlateClass(plugin?.node.type), className), }; - nodeProps = pipeInjectNodeProps(editor, nodeProps); + nodeProps = pipeInjectNodeProps( + editor, + nodeProps, + (node) => findNode(editor, { match: (n) => n === node })?.[1] as Path + ); if (nodeProps.style && Object.keys(nodeProps.style).length === 0) { delete nodeProps.style; diff --git a/packages/core/src/lib/static/serializedHtml.spec.ts b/packages/core/src/lib/static/serializedHtml.spec.ts deleted file mode 100644 index 90762150a0..0000000000 --- a/packages/core/src/lib/static/serializedHtml.spec.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { BaseBlockquotePlugin } from '@udecode/plate-block-quote'; -import { BaseHeadingPlugin } from '@udecode/plate-heading'; -import { BaseLinkPlugin } from '@udecode/plate-link'; -import { - BaseTableCellPlugin, - BaseTablePlugin, - BaseTableRowPlugin, -} from '@udecode/plate-table'; -import { decode } from 'html-entities'; - -import { createSlateEditor } from '../editor'; -import { BaseParagraphPlugin } from '../plugins'; -import { serializePlateStatic } from './serializedHtml'; - -describe('serializePlateStatic', () => { - it('should serialize paragraph to html', async () => { - const editor = createSlateEditor({ - plugins: [BaseParagraphPlugin], - value: [ - { - children: [{ text: 'Some random paragraph here...' }], - type: 'p', - }, - ], - }); - - const html = await serializePlateStatic(editor, {}); - expect(html).toContain('Some random paragraph here...'); - }); - - it('should serialize headings to html', async () => { - const editor = createSlateEditor({ - plugins: [BaseHeadingPlugin], - value: [ - { - children: [{ text: 'Heading 1' }], - type: 'h1', - }, - { - children: [{ text: 'Heading 2' }], - type: 'h2', - }, - { - children: [{ text: 'Heading 3' }], - type: 'h3', - }, - ], - }); - - const html = await serializePlateStatic(editor, {}); - expect(html).toContain('Heading 1'); - expect(html).toContain('Heading 2'); - expect(html).toContain('Heading 3'); - }); - - it('should serialize blockquote to html', async () => { - const editor = createSlateEditor({ - plugins: [BaseBlockquotePlugin], - value: [ - { - children: [{ text: 'Blockquoted text here...' }], - type: 'blockquote', - }, - ], - }); - - const html = await serializePlateStatic(editor, {}); - expect(html).toContain('Blockquoted text here...'); - }); - - it('should serialize link to html', async () => { - const editor = createSlateEditor({ - plugins: [BaseLinkPlugin], - value: [ - { children: [{ text: 'Some paragraph of text with ' }], type: 'p' }, - { - children: [{ text: 'link' }], - type: 'a', - url: 'https://example.com/', - }, - { children: [{ text: ' part.' }], type: 'p' }, - ], - }); - - const html = await serializePlateStatic(editor, {}); - expect(html).toContain(decode('href="https://example.com/"')); - expect(html).toContain('slate-a'); - }); - - // it('should serialize image to html', async () => { - // const editor = createSlateEditor({ - // plugins: [BaseImagePlugin], - // value: [ - // { - // children: [{ text: '' }], - // type: 'img', - // url: 'https://example.com/image.jpg', - // }, - // ], - // }); - - // const html = await serializePlateStatic(editor, {}); - // expect(html).toContain('src="https://example.com/image.jpg"'); - // }); - - it('should serialize table to html', async () => { - const editor = createSlateEditor({ - plugins: [BaseTablePlugin, BaseTableRowPlugin, BaseTableCellPlugin], - value: [ - { - children: [ - { - children: [ - { children: [{ text: 'Cell 1' }], type: 'td' }, - { children: [{ text: 'Cell 2' }], type: 'td' }, - ], - type: 'tr', - }, - ], - type: 'table', - }, - ], - }); - - const html = await serializePlateStatic(editor, {}); - expect(html).toContain('Cell 1'); - expect(html).toContain('Cell 2'); - }); -}); diff --git a/packages/core/src/lib/static/serializedHtml.tsx b/packages/core/src/lib/static/serializedHtml.tsx index 99b63014b4..95ecb6b668 100644 --- a/packages/core/src/lib/static/serializedHtml.tsx +++ b/packages/core/src/lib/static/serializedHtml.tsx @@ -5,6 +5,8 @@ import { decode } from 'html-entities'; import type { SlateEditor } from '../editor'; import { type StaticComponents, PlateStatic } from './components'; +import { stripHtmlClassNames } from './utils/stripHtmlClassNames'; +import { stripSlateDataAttributes } from './utils/stripSlateDataAttributes'; const getReactDOMServer = async () => { const ReactDOMServer = (await import('react-dom/server')).default; @@ -12,24 +14,51 @@ const getReactDOMServer = async () => { return ReactDOMServer; }; +export const renderComponentToHtml =

( + ReactDOMServer: any, + type: React.ComponentType

, + props: P +): string => { + return decode( + ReactDOMServer.renderToStaticMarkup(React.createElement(type, props)) + ); +}; + export const serializePlateStatic = async ( editor: SlateEditor, - staticComponents: StaticComponents + staticComponents: StaticComponents, + options: { + /** List of className prefixes to preserve from being stripped out */ + preserveClassNames?: string[]; + + /** Enable stripping class names */ + stripClassNames?: boolean; + + /** Enable stripping data attributes */ + stripDataAttributes?: boolean; + } = {} ) => { const ReactDOMServer = await getReactDOMServer(); - return renderComponentToHtml(ReactDOMServer, PlateStatic, { + let htmlString = renderComponentToHtml(ReactDOMServer, PlateStatic, { editor, staticComponents, }); -}; -export const renderComponentToHtml =

( - ReactDOMServer: any, - type: React.ComponentType

, - props: P -): string => { - return decode( - ReactDOMServer.renderToStaticMarkup(React.createElement(type, props)) - ); + const { + preserveClassNames, + stripClassNames = false, + stripDataAttributes = false, + } = options; + + if (stripClassNames) { + htmlString = stripHtmlClassNames(htmlString, { + preserveClassNames: preserveClassNames, + }); + } + if (stripDataAttributes) { + htmlString = stripSlateDataAttributes(htmlString); + } + + return htmlString; }; diff --git a/packages/core/src/lib/static/utils/stripHtmlClassNames.ts b/packages/core/src/lib/static/utils/stripHtmlClassNames.ts new file mode 100644 index 0000000000..bcc293fe08 --- /dev/null +++ b/packages/core/src/lib/static/utils/stripHtmlClassNames.ts @@ -0,0 +1,31 @@ +const classAttrRegExp = / class="([^"]*)"/g; + +/** + * Remove all class names that do not start with one of preserveClassNames + * (`slate-` by default) + */ +export const stripHtmlClassNames = ( + html: string, + { preserveClassNames = ['slate-'] }: { preserveClassNames?: string[] } +) => { + if (preserveClassNames.length === 0) { + return html.replaceAll(classAttrRegExp, ''); + } + + const preserveRegExp = new RegExp( + preserveClassNames.map((cn) => `^${cn}`).join('|') + ); + + return html.replaceAll( + classAttrRegExp, + (match: string, className: string) => { + const classesToKeep = className + .split(/\s+/) + .filter((cn) => preserveRegExp.test(cn)); + + return classesToKeep.length === 0 + ? '' + : ` class="${classesToKeep.join(' ')}"`; + } + ); +}; diff --git a/packages/core/src/lib/static/utils/stripSlateDataAttributes.ts b/packages/core/src/lib/static/utils/stripSlateDataAttributes.ts new file mode 100644 index 0000000000..38bbcf5805 --- /dev/null +++ b/packages/core/src/lib/static/utils/stripSlateDataAttributes.ts @@ -0,0 +1,5 @@ +// Remove redundant data attributes +export const stripSlateDataAttributes = (rawHtml: string): string => + rawHtml + .replaceAll(/ data-slate(?:-node|-type|-leaf|-string)="[^"]+"/g, '') + .replaceAll(/ data-testid="[^"]+"/g, '');