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 (
+ 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, '');