@unlayer/react-elements 0.1.15 → 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": {
@@ -408,6 +449,16 @@ function mapSemanticProps(props, defaultValues, componentType) {
408
449
  if (Object.keys(collected).length > 0) {
409
450
  final.border = { ...final.border || {}, ...collected };
410
451
  }
452
+ if (final.border && typeof final.border === "object") {
453
+ const b = { ...final.border };
454
+ for (const key of Object.keys(b)) {
455
+ if (!/Width$/.test(key)) continue;
456
+ const v = b[key];
457
+ if (typeof v === "number") b[key] = `${v}px`;
458
+ else if (typeof v === "string" && /^\d+(?:\.\d+)?$/.test(v.trim())) b[key] = `${v.trim()}px`;
459
+ }
460
+ final.border = b;
461
+ }
411
462
  }
412
463
  return final;
413
464
  }
@@ -1105,7 +1156,7 @@ function getWidthPercentages(cells) {
1105
1156
  return { value, className };
1106
1157
  });
1107
1158
  }
1108
- function generateGridCSS(cells, mode, contentWidth = 600, mobileBreakpoint = 620) {
1159
+ function generateGridCSS(cells, mode, contentWidth = 600, mobileBreakpoint = 480) {
1109
1160
  const widths = getWidthPercentages(cells);
1110
1161
  if (mode === "email") {
1111
1162
  const minQuery = `@media only screen and (min-width: ${contentWidth + 20}px)`;
@@ -1142,21 +1193,14 @@ ${widths.map(({ value, className }) => ` .no-stack .u-col-${className} { width:
1142
1193
  const columnCSS = widths.map(
1143
1194
  ({ value, className }) => `.u-row .u-col.u-col-${className} { flex: 0 0 ${value}%; max-width: ${value}%; }`
1144
1195
  ).join("\n");
1145
- const responsiveCSS = `
1146
- @media only screen and (max-width: ${mobileBreakpoint}px) {
1147
- .u-row { width: 100% !important; }
1148
- .u-row .u-col {
1149
- display: block !important;
1150
- width: 100% !important;
1151
- 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;
1152
1202
  max-width: 100% !important;
1153
1203
  }
1154
- .u-row .u-col > div { margin: 0 auto; }
1155
- .no-stack .u-col {
1156
- min-width: 0 !important;
1157
- display: table-cell !important;
1158
- }
1159
- ${widths.map(({ value, className }) => ` .no-stack .u-col-${className} { width: ${value}% !important; }`).join("\n")}
1160
1204
  }`;
1161
1205
  return baseCSS + "\n" + columnCSS + "\n" + responsiveCSS;
1162
1206
  }
@@ -1223,10 +1267,17 @@ var Row = (props) => {
1223
1267
  ...semanticProps
1224
1268
  } = props;
1225
1269
  const mode = modeProp ?? _config?.mode ?? "web";
1226
- let cells = propsCells || [1];
1270
+ let cells;
1227
1271
  if (layout) {
1228
1272
  validateColumnLayout(layout, React.Children.count(children));
1229
1273
  cells = layout.cells;
1274
+ } else if (propsCells) {
1275
+ cells = propsCells;
1276
+ } else {
1277
+ const columnCount = React.Children.toArray(children).filter(
1278
+ (c) => React.isValidElement(c) && /^Column$/.test(c.type?.displayName || c.type?.name || "")
1279
+ ).length;
1280
+ cells = Array(Math.max(1, columnCount)).fill(1);
1230
1281
  }
1231
1282
  const safeBodyValues = { ...DEFAULT_BODY_VALUES, ...bodyValues };
1232
1283
  const values = mapSemanticProps(
@@ -1650,8 +1701,81 @@ function extractHeadFromTree(element, options) {
1650
1701
  };
1651
1702
  }
1652
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
+
1653
1767
  // src/utils/render-to-html.tsx
1654
- 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) {
1655
1779
  const mergedConfig = { ...DEFAULT_CONFIG, ...config };
1656
1780
  try {
1657
1781
  const enriched = React.cloneElement(element, { config: mergedConfig });
@@ -1664,14 +1788,52 @@ Tip: Ensure your tree uses Body > Row > Column > Item structure.`
1664
1788
  );
1665
1789
  }
1666
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
+ }
1667
1829
  function renderToPlainText(element, config) {
1668
- const html = renderToHtml(element, config);
1830
+ const html = renderBody(element, config);
1669
1831
  return htmlToPlainText(html);
1670
1832
  }
1671
1833
  function renderToHtmlParts(element, config) {
1672
- const body = renderToHtml(element, config);
1834
+ const body = renderBody(element, config);
1673
1835
  const mergedConfig = { ...DEFAULT_CONFIG, ...config };
1674
- const displayMode = element.props.mode ?? mergedConfig.mode ?? "web";
1836
+ const displayMode = resolveDisplayMode(element, mergedConfig);
1675
1837
  const { css, js, tags } = extractHeadFromTree(element, {
1676
1838
  displayMode,
1677
1839
  headConfig: mergedConfig.headConfig