@unlayer/react-elements 0.1.16 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -826,32 +826,54 @@ declare const UnlayerProvider: React__default.FC<UnlayerProviderProps>;
826
826
  declare function useUnlayerConfig(): UnlayerConfig;
827
827
 
828
828
  /**
829
- * Renders an Unlayer element tree to an HTML string.
829
+ * Options for renderToHtml — config overrides plus document-level extras.
830
+ */
831
+ interface RenderToHtmlOptions extends Partial<UnlayerConfig> {
832
+ /** Document `<title>` */
833
+ title?: string;
834
+ /**
835
+ * Web-font stylesheets to load via `<link rel="stylesheet">` tags.
836
+ * Elements has no font registry, so pass the URLs for any non-system
837
+ * fonts your design uses (e.g. Google Fonts CSS URLs).
838
+ */
839
+ fonts?: Array<{
840
+ url: string;
841
+ }>;
842
+ }
843
+ /**
844
+ * Renders an Unlayer element tree to a complete HTML document — from
845
+ * `<!DOCTYPE ...>` to `</html>` — matching the editor's exportHtml layouts.
830
846
  *
831
- * - Passes merged config via the `config` prop (no React context — works in Server Components)
832
- * - Uses `renderToStaticMarkup` — no React hydration markers, clean HTML for email/PDF
833
- * - Synchronous
847
+ * Per-mode document shells (mirroring the editor's layout templates):
848
+ * - `email`: XHTML transitional doctype, VML/Office namespaces, and the MSO
849
+ * conditional comments Outlook needs
850
+ * - `web`: HTML5 doctype with standard viewport/charset meta tags
851
+ * - `document`: XHTML transitional doctype for print/PDF pipelines
834
852
  *
835
- * @param element - A React element tree (e.g. `<Body><Row>...</Row></Body>`)
836
- * @param config - Optional config overrides (mode, cdnBaseUrl, etc.)
837
- * @returns Clean HTML string
853
+ * Use {@link renderToHtmlParts} instead when your app owns the document
854
+ * shell and only needs the head/body chunks.
855
+ *
856
+ * @param element - A React element tree (e.g. `<Email><Row>...</Row></Email>`)
857
+ * @param options - Config overrides (mode, cdnBaseUrl, etc.) plus `title` and `fonts`
858
+ * @returns Complete HTML document string
838
859
  * @throws {Error} If rendering fails, with a helpful message
839
860
  *
840
861
  * @example
841
862
  * ```tsx
842
- * import { renderToHtml, Body, Row, Column, Paragraph } from "@unlayer/react-elements";
863
+ * import { renderToHtml, Email, Row, Column, Paragraph } from "@unlayer/react-elements";
843
864
  *
844
865
  * const html = renderToHtml(
845
- * <Body><Row><Column><Paragraph>Hello</Paragraph></Column></Row></Body>,
846
- * { mode: "email", cdnBaseUrl: "https://my-cdn.com" }
866
+ * <Email><Row><Column><Paragraph>Hello</Paragraph></Column></Row></Email>,
867
+ * { title: "Welcome" }
847
868
  * );
869
+ * // "<!DOCTYPE HTML PUBLIC ...><html ...>...</html>"
848
870
  * ```
849
871
  */
850
- declare function renderToHtml(element: React__default.ReactElement, config?: Partial<UnlayerConfig>): string;
872
+ declare function renderToHtml(element: React__default.ReactElement, options?: RenderToHtmlOptions): string;
851
873
  /**
852
874
  * Renders an Unlayer element tree to a plain text string.
853
875
  *
854
- * Internally calls `renderToHtml` then converts to plain text using
876
+ * Renders the body markup then converts to plain text using
855
877
  * `htmlToPlainText`. Useful for generating the text/plain MIME part
856
878
  * of multipart emails (critical for deliverability).
857
879
  *
@@ -876,19 +898,26 @@ declare function renderToPlainText(element: React__default.ReactElement, config?
876
898
  interface HtmlParts {
877
899
  /** `<head>` content: `<style>` blocks with component CSS, optional `<script>` tags */
878
900
  head: string;
879
- /** `<body>` content: the rendered HTML (same output as `renderToHtml`) */
901
+ /**
902
+ * The rendered body markup, wrapped in the renderer's host `<div>`. In
903
+ * email/document mode it contains the `<body>` tag itself; web mode has
904
+ * none. No doctype/html/head shell — embeddable as-is, or extract the
905
+ * `<body>` tag when composing a full document.
906
+ */
880
907
  body: string;
881
908
  }
882
909
  /**
883
910
  * Renders an Unlayer element tree to separate head and body HTML strings.
884
911
  *
885
- * This is the recommended API for sending emails, because email clients need
886
- * the `<style>` block in `<head>` for hover effects, responsive breakpoints,
887
- * and font declarations that cannot be expressed as inline styles.
912
+ * Use this when your app owns the document shell — an existing page
913
+ * template, an ESP template with its own meta tags, an iframe srcdoc —
914
+ * and you want to place the `<style>` block and body markup yourself.
915
+ * For a complete ready-to-send document in one call, use
916
+ * {@link renderToHtml}.
888
917
  *
889
918
  * - `head` contains `<style>` tags with CSS generated by each component's
890
919
  * head function (button hover colors, body fonts, link styles, etc.)
891
- * - `body` is identical to what `renderToHtml()` returns
920
+ * - `body` is the rendered body markup without any document shell
892
921
  *
893
922
  * @param element - A React element tree (e.g. `<Email><Row>...</Row></Email>`)
894
923
  * @param config - Optional config overrides (mode, cdnBaseUrl, etc.)
@@ -972,4 +1001,4 @@ declare function renderToHtmlParts(element: React__default.ReactElement, config?
972
1001
  declare function renderRowToJson(element: React__default.ReactElement): DesignRow;
973
1002
  declare function renderToJson(element: React__default.ReactElement): DesignJSON;
974
1003
 
975
- export { Body, type BorderInput, Button, type ButtonProps, Column, type ColumnLayout, ColumnLayouts, DEFAULT_CONFIG, type DesignBody, type DesignColumn, type DesignContent, type DesignJSON, type DesignRow, Divider, type DividerProps, Document, type DocumentProps, Email, type EmailProps, type FontFamilyInput, type FontWeightInput, Heading, type HeadingLevel, type HeadingProps, type Href, Html, type HtmlParts, type HtmlProps, type Icons, Image, type ImageProps, type ImageSrcInput, type LinkStyle, Menu, type MenuItem, type MenuProps, Page, type PageProps, Paragraph, type ParagraphProps, Row, type RowProps, type SizeInput, Social, type SocialIcon, type SocialProps, Table, type TableProps, type TextAlign, type TextStyleProps, type UnlayerConfig, UnlayerProvider, type UnlayerProviderProps, type ValidColumnLayout, Video, type VideoProps, type VideoSource, htmlToTextJson, renderRowToJson, renderToHtml, renderToHtmlParts, renderToJson, renderToPlainText, useUnlayerConfig, validateColumnLayout };
1004
+ export { Body, type BorderInput, Button, type ButtonProps, Column, type ColumnLayout, ColumnLayouts, DEFAULT_CONFIG, type DesignBody, type DesignColumn, type DesignContent, type DesignJSON, type DesignRow, Divider, type DividerProps, Document, type DocumentProps, Email, type EmailProps, type FontFamilyInput, type FontWeightInput, Heading, type HeadingLevel, type HeadingProps, type Href, Html, type HtmlParts, type HtmlProps, type Icons, Image, type ImageProps, type ImageSrcInput, type LinkStyle, Menu, type MenuItem, type MenuProps, Page, type PageProps, Paragraph, type ParagraphProps, type RenderToHtmlOptions, Row, type RowProps, type SizeInput, Social, type SocialIcon, type SocialProps, Table, type TableProps, type TextAlign, type TextStyleProps, type UnlayerConfig, UnlayerProvider, type UnlayerProviderProps, type ValidColumnLayout, Video, type VideoProps, type VideoSource, htmlToTextJson, renderRowToJson, renderToHtml, renderToHtmlParts, renderToJson, renderToPlainText, useUnlayerConfig, validateColumnLayout };
package/dist/index.d.ts CHANGED
@@ -826,32 +826,54 @@ declare const UnlayerProvider: React__default.FC<UnlayerProviderProps>;
826
826
  declare function useUnlayerConfig(): UnlayerConfig;
827
827
 
828
828
  /**
829
- * Renders an Unlayer element tree to an HTML string.
829
+ * Options for renderToHtml — config overrides plus document-level extras.
830
+ */
831
+ interface RenderToHtmlOptions extends Partial<UnlayerConfig> {
832
+ /** Document `<title>` */
833
+ title?: string;
834
+ /**
835
+ * Web-font stylesheets to load via `<link rel="stylesheet">` tags.
836
+ * Elements has no font registry, so pass the URLs for any non-system
837
+ * fonts your design uses (e.g. Google Fonts CSS URLs).
838
+ */
839
+ fonts?: Array<{
840
+ url: string;
841
+ }>;
842
+ }
843
+ /**
844
+ * Renders an Unlayer element tree to a complete HTML document — from
845
+ * `<!DOCTYPE ...>` to `</html>` — matching the editor's exportHtml layouts.
830
846
  *
831
- * - Passes merged config via the `config` prop (no React context — works in Server Components)
832
- * - Uses `renderToStaticMarkup` — no React hydration markers, clean HTML for email/PDF
833
- * - Synchronous
847
+ * Per-mode document shells (mirroring the editor's layout templates):
848
+ * - `email`: XHTML transitional doctype, VML/Office namespaces, and the MSO
849
+ * conditional comments Outlook needs
850
+ * - `web`: HTML5 doctype with standard viewport/charset meta tags
851
+ * - `document`: XHTML transitional doctype for print/PDF pipelines
834
852
  *
835
- * @param element - A React element tree (e.g. `<Body><Row>...</Row></Body>`)
836
- * @param config - Optional config overrides (mode, cdnBaseUrl, etc.)
837
- * @returns Clean HTML string
853
+ * Use {@link renderToHtmlParts} instead when your app owns the document
854
+ * shell and only needs the head/body chunks.
855
+ *
856
+ * @param element - A React element tree (e.g. `<Email><Row>...</Row></Email>`)
857
+ * @param options - Config overrides (mode, cdnBaseUrl, etc.) plus `title` and `fonts`
858
+ * @returns Complete HTML document string
838
859
  * @throws {Error} If rendering fails, with a helpful message
839
860
  *
840
861
  * @example
841
862
  * ```tsx
842
- * import { renderToHtml, Body, Row, Column, Paragraph } from "@unlayer/react-elements";
863
+ * import { renderToHtml, Email, Row, Column, Paragraph } from "@unlayer/react-elements";
843
864
  *
844
865
  * const html = renderToHtml(
845
- * <Body><Row><Column><Paragraph>Hello</Paragraph></Column></Row></Body>,
846
- * { mode: "email", cdnBaseUrl: "https://my-cdn.com" }
866
+ * <Email><Row><Column><Paragraph>Hello</Paragraph></Column></Row></Email>,
867
+ * { title: "Welcome" }
847
868
  * );
869
+ * // "<!DOCTYPE HTML PUBLIC ...><html ...>...</html>"
848
870
  * ```
849
871
  */
850
- declare function renderToHtml(element: React__default.ReactElement, config?: Partial<UnlayerConfig>): string;
872
+ declare function renderToHtml(element: React__default.ReactElement, options?: RenderToHtmlOptions): string;
851
873
  /**
852
874
  * Renders an Unlayer element tree to a plain text string.
853
875
  *
854
- * Internally calls `renderToHtml` then converts to plain text using
876
+ * Renders the body markup then converts to plain text using
855
877
  * `htmlToPlainText`. Useful for generating the text/plain MIME part
856
878
  * of multipart emails (critical for deliverability).
857
879
  *
@@ -876,19 +898,26 @@ declare function renderToPlainText(element: React__default.ReactElement, config?
876
898
  interface HtmlParts {
877
899
  /** `<head>` content: `<style>` blocks with component CSS, optional `<script>` tags */
878
900
  head: string;
879
- /** `<body>` content: the rendered HTML (same output as `renderToHtml`) */
901
+ /**
902
+ * The rendered body markup, wrapped in the renderer's host `<div>`. In
903
+ * email/document mode it contains the `<body>` tag itself; web mode has
904
+ * none. No doctype/html/head shell — embeddable as-is, or extract the
905
+ * `<body>` tag when composing a full document.
906
+ */
880
907
  body: string;
881
908
  }
882
909
  /**
883
910
  * Renders an Unlayer element tree to separate head and body HTML strings.
884
911
  *
885
- * This is the recommended API for sending emails, because email clients need
886
- * the `<style>` block in `<head>` for hover effects, responsive breakpoints,
887
- * and font declarations that cannot be expressed as inline styles.
912
+ * Use this when your app owns the document shell — an existing page
913
+ * template, an ESP template with its own meta tags, an iframe srcdoc —
914
+ * and you want to place the `<style>` block and body markup yourself.
915
+ * For a complete ready-to-send document in one call, use
916
+ * {@link renderToHtml}.
888
917
  *
889
918
  * - `head` contains `<style>` tags with CSS generated by each component's
890
919
  * head function (button hover colors, body fonts, link styles, etc.)
891
- * - `body` is identical to what `renderToHtml()` returns
920
+ * - `body` is the rendered body markup without any document shell
892
921
  *
893
922
  * @param element - A React element tree (e.g. `<Email><Row>...</Row></Email>`)
894
923
  * @param config - Optional config overrides (mode, cdnBaseUrl, etc.)
@@ -972,4 +1001,4 @@ declare function renderToHtmlParts(element: React__default.ReactElement, config?
972
1001
  declare function renderRowToJson(element: React__default.ReactElement): DesignRow;
973
1002
  declare function renderToJson(element: React__default.ReactElement): DesignJSON;
974
1003
 
975
- export { Body, type BorderInput, Button, type ButtonProps, Column, type ColumnLayout, ColumnLayouts, DEFAULT_CONFIG, type DesignBody, type DesignColumn, type DesignContent, type DesignJSON, type DesignRow, Divider, type DividerProps, Document, type DocumentProps, Email, type EmailProps, type FontFamilyInput, type FontWeightInput, Heading, type HeadingLevel, type HeadingProps, type Href, Html, type HtmlParts, type HtmlProps, type Icons, Image, type ImageProps, type ImageSrcInput, type LinkStyle, Menu, type MenuItem, type MenuProps, Page, type PageProps, Paragraph, type ParagraphProps, Row, type RowProps, type SizeInput, Social, type SocialIcon, type SocialProps, Table, type TableProps, type TextAlign, type TextStyleProps, type UnlayerConfig, UnlayerProvider, type UnlayerProviderProps, type ValidColumnLayout, Video, type VideoProps, type VideoSource, htmlToTextJson, renderRowToJson, renderToHtml, renderToHtmlParts, renderToJson, renderToPlainText, useUnlayerConfig, validateColumnLayout };
1004
+ export { Body, type BorderInput, Button, type ButtonProps, Column, type ColumnLayout, ColumnLayouts, DEFAULT_CONFIG, type DesignBody, type DesignColumn, type DesignContent, type DesignJSON, type DesignRow, Divider, type DividerProps, Document, type DocumentProps, Email, type EmailProps, type FontFamilyInput, type FontWeightInput, Heading, type HeadingLevel, type HeadingProps, type Href, Html, type HtmlParts, type HtmlProps, type Icons, Image, type ImageProps, type ImageSrcInput, type LinkStyle, Menu, type MenuItem, type MenuProps, Page, type PageProps, Paragraph, type ParagraphProps, type RenderToHtmlOptions, Row, type RowProps, type SizeInput, Social, type SocialIcon, type SocialProps, Table, type TableProps, type TextAlign, type TextStyleProps, type UnlayerConfig, UnlayerProvider, type UnlayerProviderProps, type ValidColumnLayout, Video, type VideoProps, type VideoSource, htmlToTextJson, renderRowToJson, renderToHtml, renderToHtmlParts, renderToJson, renderToPlainText, useUnlayerConfig, validateColumnLayout };
package/dist/index.js CHANGED
@@ -191,6 +191,50 @@ function textNodeToHtml(node) {
191
191
  }
192
192
  return html;
193
193
  }
194
+ var NUMERIC_ALIGN = {
195
+ 1: "left",
196
+ 2: "center",
197
+ 3: "right",
198
+ 4: "justify"
199
+ };
200
+ function dirAttribute(direction) {
201
+ return direction === "ltr" || direction === "rtl" ? ` dir="${direction}"` : "";
202
+ }
203
+ function paragraphAlign(format) {
204
+ if (typeof format === "string") {
205
+ return ["left", "center", "right", "justify"].includes(format) ? format : void 0;
206
+ }
207
+ if (typeof format === "number") {
208
+ return NUMERIC_ALIGN[format];
209
+ }
210
+ return void 0;
211
+ }
212
+ function paragraphNodeToHtml(node, childrenHtml) {
213
+ const isInlineTool = !!node.isInlineTool;
214
+ const tag = isInlineTool ? "span" : "p";
215
+ const dir = dirAttribute(node.direction);
216
+ const styleParts = [];
217
+ if (node.style) {
218
+ styleParts.push(String(node.style).replace(/;+\s*$/, ""));
219
+ }
220
+ if (!isInlineTool) {
221
+ styleParts.push(
222
+ "margin: 0px",
223
+ "margin-block-start: 0px",
224
+ "margin-block-end: 0px"
225
+ );
226
+ }
227
+ const indent = Number(node.indent) || 0;
228
+ if (indent > 0) {
229
+ styleParts.push(`padding-inline-start: ${indent * 40}px`);
230
+ }
231
+ const align = paragraphAlign(node.format);
232
+ if (align) {
233
+ styleParts.push(`text-align: ${align}`);
234
+ }
235
+ const style = styleParts.length > 0 ? ` style="${escapeHtml(styleParts.join("; ") + ";")}"` : "";
236
+ return `<${tag}${dir}${style}>${childrenHtml || "<br>"}</${tag}>`;
237
+ }
194
238
  function nodeToHtml(node) {
195
239
  if (!node || typeof node !== "object") return "";
196
240
  const type = node.type;
@@ -205,14 +249,11 @@ function nodeToHtml(node) {
205
249
  case "root":
206
250
  return childrenHtml;
207
251
  case "paragraph":
208
- case "extended-paragraph": {
209
- if (!childrenHtml) return "<p><br></p>";
210
- const dir = node.direction ? ` dir="${node.direction}"` : "";
211
- return `<p${dir}>${childrenHtml}</p>`;
212
- }
252
+ case "extended-paragraph":
253
+ return paragraphNodeToHtml(node, childrenHtml);
213
254
  case "heading": {
214
- const tag = node.tag || "h1";
215
- const dir = node.direction ? ` dir="${node.direction}"` : "";
255
+ const tag = /^h[1-6]$/.test(node.tag) ? node.tag : "h1";
256
+ const dir = dirAttribute(node.direction);
216
257
  return `<${tag}${dir}>${childrenHtml}</${tag}>`;
217
258
  }
218
259
  case "link": {
@@ -1115,7 +1156,7 @@ function getWidthPercentages(cells) {
1115
1156
  return { value, className };
1116
1157
  });
1117
1158
  }
1118
- function generateGridCSS(cells, mode, contentWidth = 600, mobileBreakpoint = 620) {
1159
+ function generateGridCSS(cells, mode, contentWidth = 600, mobileBreakpoint = 480) {
1119
1160
  const widths = getWidthPercentages(cells);
1120
1161
  if (mode === "email") {
1121
1162
  const minQuery = `@media only screen and (min-width: ${contentWidth + 20}px)`;
@@ -1152,21 +1193,14 @@ ${widths.map(({ value, className }) => ` .no-stack .u-col-${className} { width:
1152
1193
  const columnCSS = widths.map(
1153
1194
  ({ value, className }) => `.u-row .u-col.u-col-${className} { flex: 0 0 ${value}%; max-width: ${value}%; }`
1154
1195
  ).join("\n");
1155
- const responsiveCSS = `
1156
- @media only screen and (max-width: ${mobileBreakpoint}px) {
1157
- .u-row { width: 100% !important; }
1158
- .u-row .u-col {
1159
- display: block !important;
1160
- width: 100% !important;
1161
- min-width: 320px !important;
1196
+ const responsiveCSS = mode === "document" ? "" : `
1197
+ @media (max-width: ${mobileBreakpoint}px) {
1198
+ .u_row .container { max-width: 100% !important; }
1199
+ .u-row:not(.no-stack) { flex-wrap: wrap; }
1200
+ .u-row:not(.no-stack) .u-col {
1201
+ flex: 0 0 100% !important;
1162
1202
  max-width: 100% !important;
1163
1203
  }
1164
- .u-row .u-col > div { margin: 0 auto; }
1165
- .no-stack .u-col {
1166
- min-width: 0 !important;
1167
- display: table-cell !important;
1168
- }
1169
- ${widths.map(({ value, className }) => ` .no-stack .u-col-${className} { width: ${value}% !important; }`).join("\n")}
1170
1204
  }`;
1171
1205
  return baseCSS + "\n" + columnCSS + "\n" + responsiveCSS;
1172
1206
  }
@@ -1667,8 +1701,81 @@ function extractHeadFromTree(element, options) {
1667
1701
  };
1668
1702
  }
1669
1703
 
1704
+ // src/utils/document-layouts.ts
1705
+ function emailLayout(args) {
1706
+ const { dir, titleTag, tagLines, styleTag, scriptTag, fontLinks, body } = args;
1707
+ const head = [
1708
+ titleTag,
1709
+ tagLines,
1710
+ styleTag,
1711
+ scriptTag,
1712
+ fontLinks && `<!--[if !mso]><!-->${fontLinks}<!--<![endif]-->`
1713
+ ].filter(Boolean).join("\n ");
1714
+ return `<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1715
+ <html${dir} xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
1716
+ <head>
1717
+ <!--[if gte mso 9]>
1718
+ <xml>
1719
+ <o:OfficeDocumentSettings>
1720
+ <o:AllowPNG/>
1721
+ <o:PixelsPerInch>96</o:PixelsPerInch>
1722
+ </o:OfficeDocumentSettings>
1723
+ </xml>
1724
+ <![endif]-->
1725
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
1726
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1727
+ <meta name="x-apple-disable-message-reformatting">
1728
+ <!--[if !mso]><!--><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]-->
1729
+ ${head}
1730
+ </head>
1731
+ ${body}
1732
+ </html>
1733
+ `;
1734
+ }
1735
+ function documentLayout(args) {
1736
+ const { dir, titleTag, tagLines, styleTag, scriptTag, fontLinks, body } = args;
1737
+ const head = [titleTag, tagLines, styleTag, scriptTag, fontLinks].filter(Boolean).join("\n ");
1738
+ return `<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1739
+ <html${dir} xmlns="http://www.w3.org/1999/xhtml">
1740
+ <head>
1741
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
1742
+ ${head}
1743
+ </head>
1744
+ ${body}
1745
+ </html>
1746
+ `;
1747
+ }
1748
+ function webLayout(args) {
1749
+ const { dir, titleTag, tagLines, styleTag, scriptTag, fontLinks, body } = args;
1750
+ const head = [titleTag, fontLinks, tagLines, styleTag, scriptTag].filter(Boolean).join("\n ");
1751
+ return `<!doctype html>
1752
+ <html${dir}>
1753
+ <head>
1754
+ <meta charset="utf-8">
1755
+ <meta http-equiv="x-ua-compatible" content="ie=edge">
1756
+ <meta name="description" content="">
1757
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1758
+ ${head}
1759
+ </head>
1760
+ <body>
1761
+ ${body}
1762
+ </body>
1763
+ </html>
1764
+ `;
1765
+ }
1766
+
1670
1767
  // src/utils/render-to-html.tsx
1671
- function renderToHtml(element, config) {
1768
+ var MODE_BY_WRAPPER = {
1769
+ Email: "email",
1770
+ Page: "web",
1771
+ Document: "document"
1772
+ };
1773
+ function resolveDisplayMode(element, mergedConfig) {
1774
+ const displayName = element.type?.displayName;
1775
+ const wrapperMode = displayName ? MODE_BY_WRAPPER[displayName] : void 0;
1776
+ return wrapperMode ?? element.props.mode ?? mergedConfig.mode ?? "web";
1777
+ }
1778
+ function renderBody(element, config) {
1672
1779
  const mergedConfig = { ...DEFAULT_CONFIG, ...config };
1673
1780
  try {
1674
1781
  const enriched = React.cloneElement(element, { config: mergedConfig });
@@ -1681,14 +1788,52 @@ Tip: Ensure your tree uses Body > Row > Column > Item structure.`
1681
1788
  );
1682
1789
  }
1683
1790
  }
1791
+ function stripOuterDiv(html) {
1792
+ const match = html.match(/^\s*<div[^>]*>([\s\S]*)<\/div>\s*$/);
1793
+ return match ? match[1] : html;
1794
+ }
1795
+ function escapeForHtml(text) {
1796
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1797
+ }
1798
+ function renderToHtml(element, options) {
1799
+ const { title, fonts = [], ...config } = options ?? {};
1800
+ const mergedConfig = { ...DEFAULT_CONFIG, ...config };
1801
+ const displayMode = resolveDisplayMode(element, mergedConfig);
1802
+ const wrapperName = element.type?.displayName;
1803
+ const isUnlayerWrapper = wrapperName === "Body" || (wrapperName ? wrapperName in MODE_BY_WRAPPER : false);
1804
+ const rawBody = renderBody(element, config);
1805
+ const body = isUnlayerWrapper ? stripOuterDiv(rawBody) : rawBody;
1806
+ const { css, js, tags } = extractHeadFromTree(element, {
1807
+ displayMode,
1808
+ headConfig: mergedConfig.headConfig
1809
+ });
1810
+ const layoutArgs = {
1811
+ dir: mergedConfig.textDirection ? ` dir="${escapeForHtml(String(mergedConfig.textDirection))}"` : "",
1812
+ titleTag: title ? `<title>${escapeForHtml(title.trim())}</title>` : "",
1813
+ styleTag: css ? `<style type="text/css">
1814
+ ${css}
1815
+ </style>` : "",
1816
+ scriptTag: js ? `<script type="application/javascript">
1817
+ ${js}
1818
+ </script>` : "",
1819
+ tagLines: tags.join("\n"),
1820
+ fontLinks: fonts.filter((font) => font?.url).map(
1821
+ (font) => `<link href="${escapeForHtml(font.url)}" rel="stylesheet" type="text/css">`
1822
+ ).join(""),
1823
+ body
1824
+ };
1825
+ if (displayMode === "email") return emailLayout(layoutArgs);
1826
+ if (displayMode === "document") return documentLayout(layoutArgs);
1827
+ return webLayout(layoutArgs);
1828
+ }
1684
1829
  function renderToPlainText(element, config) {
1685
- const html = renderToHtml(element, config);
1830
+ const html = renderBody(element, config);
1686
1831
  return htmlToPlainText(html);
1687
1832
  }
1688
1833
  function renderToHtmlParts(element, config) {
1689
- const body = renderToHtml(element, config);
1834
+ const body = renderBody(element, config);
1690
1835
  const mergedConfig = { ...DEFAULT_CONFIG, ...config };
1691
- const displayMode = element.props.mode ?? mergedConfig.mode ?? "web";
1836
+ const displayMode = resolveDisplayMode(element, mergedConfig);
1692
1837
  const { css, js, tags } = extractHeadFromTree(element, {
1693
1838
  displayMode,
1694
1839
  headConfig: mergedConfig.headConfig