react-email 6.6.6 → 6.6.8

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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # react-email
2
2
 
3
+ ## 6.6.8
4
+
5
+ ### Patch Changes
6
+
7
+ - dca3c01: Fix `email build` computing the wrong output file tracing root in nested-workspace monorepos (e.g. a package one or more directories below the true repo root), which caused Vercel deploys to fail with an ENOENT error on the routes manifest.
8
+
9
+ ## 6.6.7
10
+
11
+ ### Patch Changes
12
+
13
+ - 6a5ff2a: Escape double quotes in `Markdown` link `href`/`title` and image `title` attributes, matching the escaping already applied to image `src`/`alt`. A markdown title like `'The "Complete" Guide'` no longer breaks out of the attribute in the rendered HTML.
14
+ - 4cf4c72: Fix two-value logical shorthands whose values aren't all numeric (e.g. `margin-inline: 1rem auto`, `padding-inline: 10px calc(1rem + 2px)`) producing invalid duplicated longhands. They are now split into the correct per-side declarations.
15
+ - fa77d55: Merge declarations when the same class is defined by multiple Tailwind rules (e.g. a preset and a child config override).
16
+ - fa52a04: Convert Tailwind's `rgba(r g b / a)` syntax to `rgb(r,g,b,a)` syntax for better email client support.
17
+ - fc8318c: Fix Tailwind classes not being inlined into styles for `<Section>`, `<Column>` and `<Row>`.
18
+
3
19
  ## 6.6.6
4
20
 
5
21
  ### Patch Changes
@@ -7,6 +7,8 @@ import fs, { existsSync, promises, statSync } from "node:fs";
7
7
  import * as path$2 from "node:path";
8
8
  import path from "node:path";
9
9
  import url, { fileURLToPath } from "node:url";
10
+ import logSymbols from "log-symbols";
11
+ import { addDevDependency, installDependencies, runScript } from "nypm";
10
12
  import * as fsp$1 from "node:fs/promises";
11
13
  import fsp from "node:fs/promises";
12
14
  import { F_OK } from "node:constants";
@@ -15,8 +17,6 @@ import nativeFs from "fs";
15
17
  import path$1, { basename, dirname, normalize, posix, relative, resolve, sep } from "path";
16
18
  import { fileURLToPath as fileURLToPath$1 } from "url";
17
19
  import { createRequire as createRequire$1 } from "module";
18
- import logSymbols from "log-symbols";
19
- import { addDevDependency, installDependencies, runScript } from "nypm";
20
20
  import { createJiti } from "jiti";
21
21
  import prompts from "prompts";
22
22
  import { Spinner } from "picospinner";
@@ -59,6 +59,64 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
59
59
  }) : target, mod));
60
60
  var __require$1 = /* @__PURE__ */ createRequire(import.meta.url);
61
61
  //#endregion
62
+ //#region src/cli/utils/get-emails-directory-metadata.ts
63
+ const isFileAnEmail = async (fullPath) => {
64
+ let fileHandle;
65
+ try {
66
+ fileHandle = await fs.promises.open(fullPath, "r");
67
+ } catch (exception) {
68
+ console.warn(exception);
69
+ return false;
70
+ }
71
+ if ((await fileHandle.stat()).isDirectory()) {
72
+ await fileHandle.close();
73
+ return false;
74
+ }
75
+ const { ext } = path.parse(fullPath);
76
+ if (![
77
+ ".js",
78
+ ".tsx",
79
+ ".jsx"
80
+ ].includes(ext)) {
81
+ await fileHandle.close();
82
+ return false;
83
+ }
84
+ const fileContents = await fileHandle.readFile("utf8");
85
+ await fileHandle.close();
86
+ const hasES6DefaultExport = /\bexport\s+default\b/gm.test(fileContents);
87
+ const hasCommonJSExport = /\bmodule\.exports\s*=/gm.test(fileContents);
88
+ const hasNamedExport = /\bexport\s+\{[^}]*\bdefault\b[^}]*\}/gm.test(fileContents);
89
+ return hasES6DefaultExport || hasCommonJSExport || hasNamedExport;
90
+ };
91
+ const mergeDirectoriesWithSubDirectories = (emailsDirectoryMetadata) => {
92
+ let currentResultingMergedDirectory = emailsDirectoryMetadata;
93
+ while (currentResultingMergedDirectory.emailFilenames.length === 0 && currentResultingMergedDirectory.subDirectories.length === 1) {
94
+ const onlySubDirectory = currentResultingMergedDirectory.subDirectories[0];
95
+ currentResultingMergedDirectory = {
96
+ ...onlySubDirectory,
97
+ directoryName: path.join(currentResultingMergedDirectory.directoryName, onlySubDirectory.directoryName)
98
+ };
99
+ }
100
+ return currentResultingMergedDirectory;
101
+ };
102
+ const getEmailsDirectoryMetadata = async (absolutePathToEmailsDirectory, keepFileExtensions = false, isSubDirectory = false, baseDirectoryPath = absolutePathToEmailsDirectory) => {
103
+ if (!fs.existsSync(absolutePathToEmailsDirectory)) return;
104
+ const dirents = await fs.promises.readdir(absolutePathToEmailsDirectory, { withFileTypes: true });
105
+ const isEmailPredicates = await Promise.all(dirents.map((dirent) => isFileAnEmail(path.join(absolutePathToEmailsDirectory, dirent.name))));
106
+ const emailFilenames = dirents.filter((_, i) => isEmailPredicates[i]).map((dirent) => keepFileExtensions ? dirent.name : dirent.name.replace(path.extname(dirent.name), ""));
107
+ const subDirectories = await Promise.all(dirents.filter((dirent) => dirent.isDirectory() && !dirent.name.startsWith("_") && dirent.name !== "static").map((dirent) => {
108
+ return getEmailsDirectoryMetadata(path.join(absolutePathToEmailsDirectory, dirent.name), keepFileExtensions, true, baseDirectoryPath);
109
+ }));
110
+ const emailsMetadata = {
111
+ absolutePath: absolutePathToEmailsDirectory,
112
+ relativePath: path.relative(baseDirectoryPath, absolutePathToEmailsDirectory),
113
+ directoryName: absolutePathToEmailsDirectory.split(path.sep).pop(),
114
+ emailFilenames,
115
+ subDirectories
116
+ };
117
+ return isSubDirectory ? mergeDirectoriesWithSubDirectories(emailsMetadata) : emailsMetadata;
118
+ };
119
+ //#endregion
62
120
  //#region ../../node_modules/.pnpm/fdir@6.5.0_picomatch@4.0.4/node_modules/fdir/dist/index.mjs
63
121
  var __require = /* @__PURE__ */ createRequire$1(import.meta.url);
64
122
  function cleanPath(path) {
@@ -6462,68 +6520,20 @@ function validatePackages(packages) {
6462
6520
  }
6463
6521
  }
6464
6522
  //#endregion
6465
- //#region src/cli/utils/get-emails-directory-metadata.ts
6466
- const isFileAnEmail = async (fullPath) => {
6467
- let fileHandle;
6523
+ //#region src/cli/utils/get-tracing-root-dir.ts
6524
+ const getTracingRootDir = async (usersProjectLocation) => {
6468
6525
  try {
6469
- fileHandle = await fs.promises.open(fullPath, "r");
6470
- } catch (exception) {
6471
- console.warn(exception);
6472
- return false;
6473
- }
6474
- if ((await fileHandle.stat()).isDirectory()) {
6475
- await fileHandle.close();
6476
- return false;
6477
- }
6478
- const { ext } = path.parse(fullPath);
6479
- if (![
6480
- ".js",
6481
- ".tsx",
6482
- ".jsx"
6483
- ].includes(ext)) {
6484
- await fileHandle.close();
6485
- return false;
6486
- }
6487
- const fileContents = await fileHandle.readFile("utf8");
6488
- await fileHandle.close();
6489
- const hasES6DefaultExport = /\bexport\s+default\b/gm.test(fileContents);
6490
- const hasCommonJSExport = /\bmodule\.exports\s*=/gm.test(fileContents);
6491
- const hasNamedExport = /\bexport\s+\{[^}]*\bdefault\b[^}]*\}/gm.test(fileContents);
6492
- return hasES6DefaultExport || hasCommonJSExport || hasNamedExport;
6493
- };
6494
- const mergeDirectoriesWithSubDirectories = (emailsDirectoryMetadata) => {
6495
- let currentResultingMergedDirectory = emailsDirectoryMetadata;
6496
- while (currentResultingMergedDirectory.emailFilenames.length === 0 && currentResultingMergedDirectory.subDirectories.length === 1) {
6497
- const onlySubDirectory = currentResultingMergedDirectory.subDirectories[0];
6498
- currentResultingMergedDirectory = {
6499
- ...onlySubDirectory,
6500
- directoryName: path.join(currentResultingMergedDirectory.directoryName, onlySubDirectory.directoryName)
6501
- };
6526
+ const { rootDir } = await getPackages(usersProjectLocation);
6527
+ return rootDir.replaceAll("\\", "/");
6528
+ } catch {
6529
+ return usersProjectLocation.replaceAll("\\", "/");
6502
6530
  }
6503
- return currentResultingMergedDirectory;
6504
- };
6505
- const getEmailsDirectoryMetadata = async (absolutePathToEmailsDirectory, keepFileExtensions = false, isSubDirectory = false, baseDirectoryPath = absolutePathToEmailsDirectory) => {
6506
- if (!fs.existsSync(absolutePathToEmailsDirectory)) return;
6507
- const dirents = await fs.promises.readdir(absolutePathToEmailsDirectory, { withFileTypes: true });
6508
- const isEmailPredicates = await Promise.all(dirents.map((dirent) => isFileAnEmail(path.join(absolutePathToEmailsDirectory, dirent.name))));
6509
- const emailFilenames = dirents.filter((_, i) => isEmailPredicates[i]).map((dirent) => keepFileExtensions ? dirent.name : dirent.name.replace(path.extname(dirent.name), ""));
6510
- const subDirectories = await Promise.all(dirents.filter((dirent) => dirent.isDirectory() && !dirent.name.startsWith("_") && dirent.name !== "static").map((dirent) => {
6511
- return getEmailsDirectoryMetadata(path.join(absolutePathToEmailsDirectory, dirent.name), keepFileExtensions, true, baseDirectoryPath);
6512
- }));
6513
- const emailsMetadata = {
6514
- absolutePath: absolutePathToEmailsDirectory,
6515
- relativePath: path.relative(baseDirectoryPath, absolutePathToEmailsDirectory),
6516
- directoryName: absolutePathToEmailsDirectory.split(path.sep).pop(),
6517
- emailFilenames,
6518
- subDirectories
6519
- };
6520
- return isSubDirectory ? mergeDirectoriesWithSubDirectories(emailsMetadata) : emailsMetadata;
6521
6531
  };
6522
6532
  //#endregion
6523
6533
  //#region package.json
6524
6534
  var package_default = {
6525
6535
  name: "react-email",
6526
- version: "6.6.6",
6536
+ version: "6.6.8",
6527
6537
  description: "A live preview of your emails right in your browser.",
6528
6538
  bin: { "email": "./dist/cli/index.mjs" },
6529
6539
  type: "module",
@@ -6729,14 +6739,13 @@ const stopSpinnerAndPersist = (spinner, display) => {
6729
6739
  //#region src/cli/commands/build.ts
6730
6740
  const isInReactEmailMonorepo = !path.dirname(fileURLToPath(import.meta.url)).includes("node_modules");
6731
6741
  const setNextEnvironmentVariablesForBuild = async (emailsDirRelativePath, builtPreviewAppPath, usersProjectLocation) => {
6732
- let rootDir = "userProjectLocation";
6733
- if (isInReactEmailMonorepo) rootDir = `'${await getPackages(usersProjectLocation).then((p) => p.rootDir.replaceAll("\\", "/"))}'`;
6742
+ const rootDir = await getTracingRootDir(usersProjectLocation);
6734
6743
  const nextConfigContents = `
6735
6744
  import path from 'path';
6736
6745
  const emailsDirRelativePath = path.normalize('${emailsDirRelativePath}');
6737
6746
  const userProjectLocation = '${process.cwd().replaceAll("\\", "/")}';
6738
6747
  const previewServerLocation = '${builtPreviewAppPath.replaceAll("\\", "/")}';
6739
- const rootDir = ${rootDir};
6748
+ const rootDir = '${rootDir}';
6740
6749
  /** @type {import('next').NextConfig} */
6741
6750
  const nextConfig = {
6742
6751
  env: {
package/dist/index.cjs CHANGED
@@ -17821,11 +17821,11 @@ const Markdown = react.forwardRef(({ children, markdownContainerStyles, markdown
17821
17821
  return `<hr${parseCssInJsToInlineCss(finalStyles.hr) !== "" ? ` style="${parseCssInJsToInlineCss(finalStyles.hr)}"` : ""} />\n`;
17822
17822
  };
17823
17823
  renderer.image = ({ href, text, title }) => {
17824
- return `<img src="${href.replaceAll("\"", "&quot;")}" alt="${text.replaceAll("\"", "&quot;")}"${title ? ` title="${title}"` : ""}${parseCssInJsToInlineCss(finalStyles.image) !== "" ? ` style="${parseCssInJsToInlineCss(finalStyles.image)}"` : ""}>`;
17824
+ return `<img src="${href.replaceAll("\"", "&quot;")}" alt="${text.replaceAll("\"", "&quot;")}"${title ? ` title="${title.replaceAll("\"", "&quot;")}"` : ""}${parseCssInJsToInlineCss(finalStyles.image) !== "" ? ` style="${parseCssInJsToInlineCss(finalStyles.image)}"` : ""}>`;
17825
17825
  };
17826
17826
  renderer.link = ({ href, title, tokens }) => {
17827
17827
  const text = renderer.parser.parseInline(tokens);
17828
- return `<a href="${href}" target="_blank"${title ? ` title="${title}"` : ""}${parseCssInJsToInlineCss(finalStyles.link) !== "" ? ` style="${parseCssInJsToInlineCss(finalStyles.link)}"` : ""}>${text}</a>`;
17828
+ return `<a href="${href.replaceAll("\"", "&quot;")}" target="_blank"${title ? ` title="${title.replaceAll("\"", "&quot;")}"` : ""}${parseCssInJsToInlineCss(finalStyles.link) !== "" ? ` style="${parseCssInJsToInlineCss(finalStyles.link)}"` : ""}>${text}</a>`;
17829
17829
  };
17830
17830
  renderer.listitem = ({ tokens, loose }) => {
17831
17831
  const hasNestedList = tokens.some((token) => token.type === "list");
@@ -37702,6 +37702,11 @@ function extractRulesPerClass(root, classes) {
37702
37702
  const classSet = new Set(classes);
37703
37703
  const inlinableRules = /* @__PURE__ */ new Map();
37704
37704
  const nonInlinableRules = /* @__PURE__ */ new Map();
37705
+ const appendRule = (map, className, rule) => {
37706
+ const existing = map.get(className);
37707
+ if (existing) existing.push(rule);
37708
+ else map.set(className, [rule]);
37709
+ };
37705
37710
  walk(root, {
37706
37711
  visit: "Rule",
37707
37712
  enter(rule) {
@@ -37713,13 +37718,13 @@ function extractRulesPerClass(root, classes) {
37713
37718
  }
37714
37719
  });
37715
37720
  if (isRuleInlinable(rule)) {
37716
- for (const className of selectorClasses) if (classSet.has(className)) inlinableRules.set(className, rule);
37721
+ for (const className of selectorClasses) if (classSet.has(className)) appendRule(inlinableRules, className, rule);
37717
37722
  } else {
37718
37723
  const { inlinablePart, nonInlinablePart } = splitMixedRule(rule);
37719
37724
  for (const className of selectorClasses) {
37720
37725
  if (!classSet.has(className)) continue;
37721
- if (inlinablePart) inlinableRules.set(className, inlinablePart);
37722
- if (nonInlinablePart) nonInlinableRules.set(className, nonInlinablePart);
37726
+ if (inlinablePart) appendRule(inlinableRules, className, inlinablePart);
37727
+ if (nonInlinablePart) appendRule(nonInlinableRules, className, nonInlinablePart);
37723
37728
  }
37724
37729
  }
37725
37730
  }
@@ -37880,7 +37885,7 @@ function makeInlineStylesFor(inlinableRules, customProperties) {
37880
37885
  function inlineStyles(styleSheet, classes) {
37881
37886
  const { inlinable: inlinableRules } = extractRulesPerClass(styleSheet, classes);
37882
37887
  const customProperties = getCustomProperties(styleSheet);
37883
- return makeInlineStylesFor(Array.from(inlinableRules.values()), customProperties);
37888
+ return makeInlineStylesFor(Array.from(inlinableRules.values()).flat(), customProperties);
37884
37889
  }
37885
37890
  //#endregion
37886
37891
  //#region src/components/tailwind/utils/css/resolve-all-css-variables.ts
@@ -38152,7 +38157,7 @@ function oklchToRgb(oklch) {
38152
38157
  }
38153
38158
  function separteShorthandDeclaration(shorthandToReplace, [start, end]) {
38154
38159
  shorthandToReplace.property = start;
38155
- const values = shorthandToReplace.value.type === "Value" ? shorthandToReplace.value.children.toArray().filter((child) => child.type === "Dimension" || child.type === "Number" || child.type === "Percentage") : [shorthandToReplace.value];
38160
+ const values = shorthandToReplace.value.type === "Value" ? shorthandToReplace.value.children.toArray().filter((child) => child.type !== "Operator" && child.type !== "WhiteSpace") : [shorthandToReplace.value];
38156
38161
  let endValue = shorthandToReplace.value;
38157
38162
  if (values.length === 2) {
38158
38163
  endValue = {
@@ -38240,7 +38245,7 @@ function sanitizeDeclarations(nodeContainingDeclarations) {
38240
38245
  });
38241
38246
  funcParentListItem.data = rgbNode(rgb.r, rgb.g, rgb.b, a);
38242
38247
  }
38243
- if (func.name === "rgb") {
38248
+ if (func.name === "rgb" || func.name === "rgba") {
38244
38249
  let r;
38245
38250
  let g;
38246
38251
  let b;
@@ -38676,12 +38681,15 @@ const componentsToTreatAsElements = [
38676
38681
  Button,
38677
38682
  CodeBlock,
38678
38683
  CodeInline,
38684
+ Column,
38679
38685
  Container,
38680
38686
  Heading,
38681
38687
  Hr,
38682
38688
  Img,
38683
38689
  Link,
38684
38690
  Preview,
38691
+ Row,
38692
+ Section,
38685
38693
  Text
38686
38694
  ];
38687
38695
  const isComponent = (element) => {
@@ -38723,10 +38731,10 @@ function cloneElementWithInlinedStyles(element, inlinableRules, nonInlinableRule
38723
38731
  const residualClasses = [];
38724
38732
  const rules = [];
38725
38733
  for (const className of classes) {
38726
- const rule = inlinableRules.get(className);
38727
- if (rule) rules.push(rule);
38734
+ const classRules = inlinableRules.get(className);
38735
+ if (classRules) rules.push(...classRules);
38728
38736
  if (nonInlinableRules.has(className)) residualClasses.push(className);
38729
- else if (!rule) residualClasses.push(className);
38737
+ else if (!classRules) residualClasses.push(className);
38730
38738
  }
38731
38739
  propsToOverwrite.style = {
38732
38740
  ...makeInlineStylesFor(rules, customProperties),
@@ -40663,7 +40671,7 @@ function Tailwind({ children, config, theme, utility }) {
40663
40671
  const customProperties = getCustomProperties(styleSheet);
40664
40672
  const nonInlineStyles = {
40665
40673
  type: "StyleSheet",
40666
- children: new List().fromArray(Array.from(nonInlinableRules.values()))
40674
+ children: new List().fromArray(Array.from(nonInlinableRules.values()).flat())
40667
40675
  };
40668
40676
  sanitizeNonInlinableRules(nonInlineStyles);
40669
40677
  downlevelForEmailClients(nonInlineStyles);
package/dist/index.mjs CHANGED
@@ -17800,11 +17800,11 @@ const Markdown = React$1.forwardRef(({ children, markdownContainerStyles, markdo
17800
17800
  return `<hr${parseCssInJsToInlineCss(finalStyles.hr) !== "" ? ` style="${parseCssInJsToInlineCss(finalStyles.hr)}"` : ""} />\n`;
17801
17801
  };
17802
17802
  renderer.image = ({ href, text, title }) => {
17803
- return `<img src="${href.replaceAll("\"", "&quot;")}" alt="${text.replaceAll("\"", "&quot;")}"${title ? ` title="${title}"` : ""}${parseCssInJsToInlineCss(finalStyles.image) !== "" ? ` style="${parseCssInJsToInlineCss(finalStyles.image)}"` : ""}>`;
17803
+ return `<img src="${href.replaceAll("\"", "&quot;")}" alt="${text.replaceAll("\"", "&quot;")}"${title ? ` title="${title.replaceAll("\"", "&quot;")}"` : ""}${parseCssInJsToInlineCss(finalStyles.image) !== "" ? ` style="${parseCssInJsToInlineCss(finalStyles.image)}"` : ""}>`;
17804
17804
  };
17805
17805
  renderer.link = ({ href, title, tokens }) => {
17806
17806
  const text = renderer.parser.parseInline(tokens);
17807
- return `<a href="${href}" target="_blank"${title ? ` title="${title}"` : ""}${parseCssInJsToInlineCss(finalStyles.link) !== "" ? ` style="${parseCssInJsToInlineCss(finalStyles.link)}"` : ""}>${text}</a>`;
17807
+ return `<a href="${href.replaceAll("\"", "&quot;")}" target="_blank"${title ? ` title="${title.replaceAll("\"", "&quot;")}"` : ""}${parseCssInJsToInlineCss(finalStyles.link) !== "" ? ` style="${parseCssInJsToInlineCss(finalStyles.link)}"` : ""}>${text}</a>`;
17808
17808
  };
17809
17809
  renderer.listitem = ({ tokens, loose }) => {
17810
17810
  const hasNestedList = tokens.some((token) => token.type === "list");
@@ -37681,6 +37681,11 @@ function extractRulesPerClass(root, classes) {
37681
37681
  const classSet = new Set(classes);
37682
37682
  const inlinableRules = /* @__PURE__ */ new Map();
37683
37683
  const nonInlinableRules = /* @__PURE__ */ new Map();
37684
+ const appendRule = (map, className, rule) => {
37685
+ const existing = map.get(className);
37686
+ if (existing) existing.push(rule);
37687
+ else map.set(className, [rule]);
37688
+ };
37684
37689
  walk(root, {
37685
37690
  visit: "Rule",
37686
37691
  enter(rule) {
@@ -37692,13 +37697,13 @@ function extractRulesPerClass(root, classes) {
37692
37697
  }
37693
37698
  });
37694
37699
  if (isRuleInlinable(rule)) {
37695
- for (const className of selectorClasses) if (classSet.has(className)) inlinableRules.set(className, rule);
37700
+ for (const className of selectorClasses) if (classSet.has(className)) appendRule(inlinableRules, className, rule);
37696
37701
  } else {
37697
37702
  const { inlinablePart, nonInlinablePart } = splitMixedRule(rule);
37698
37703
  for (const className of selectorClasses) {
37699
37704
  if (!classSet.has(className)) continue;
37700
- if (inlinablePart) inlinableRules.set(className, inlinablePart);
37701
- if (nonInlinablePart) nonInlinableRules.set(className, nonInlinablePart);
37705
+ if (inlinablePart) appendRule(inlinableRules, className, inlinablePart);
37706
+ if (nonInlinablePart) appendRule(nonInlinableRules, className, nonInlinablePart);
37702
37707
  }
37703
37708
  }
37704
37709
  }
@@ -37859,7 +37864,7 @@ function makeInlineStylesFor(inlinableRules, customProperties) {
37859
37864
  function inlineStyles(styleSheet, classes) {
37860
37865
  const { inlinable: inlinableRules } = extractRulesPerClass(styleSheet, classes);
37861
37866
  const customProperties = getCustomProperties(styleSheet);
37862
- return makeInlineStylesFor(Array.from(inlinableRules.values()), customProperties);
37867
+ return makeInlineStylesFor(Array.from(inlinableRules.values()).flat(), customProperties);
37863
37868
  }
37864
37869
  //#endregion
37865
37870
  //#region src/components/tailwind/utils/css/resolve-all-css-variables.ts
@@ -38131,7 +38136,7 @@ function oklchToRgb(oklch) {
38131
38136
  }
38132
38137
  function separteShorthandDeclaration(shorthandToReplace, [start, end]) {
38133
38138
  shorthandToReplace.property = start;
38134
- const values = shorthandToReplace.value.type === "Value" ? shorthandToReplace.value.children.toArray().filter((child) => child.type === "Dimension" || child.type === "Number" || child.type === "Percentage") : [shorthandToReplace.value];
38139
+ const values = shorthandToReplace.value.type === "Value" ? shorthandToReplace.value.children.toArray().filter((child) => child.type !== "Operator" && child.type !== "WhiteSpace") : [shorthandToReplace.value];
38135
38140
  let endValue = shorthandToReplace.value;
38136
38141
  if (values.length === 2) {
38137
38142
  endValue = {
@@ -38219,7 +38224,7 @@ function sanitizeDeclarations(nodeContainingDeclarations) {
38219
38224
  });
38220
38225
  funcParentListItem.data = rgbNode(rgb.r, rgb.g, rgb.b, a);
38221
38226
  }
38222
- if (func.name === "rgb") {
38227
+ if (func.name === "rgb" || func.name === "rgba") {
38223
38228
  let r;
38224
38229
  let g;
38225
38230
  let b;
@@ -38655,12 +38660,15 @@ const componentsToTreatAsElements = [
38655
38660
  Button,
38656
38661
  CodeBlock,
38657
38662
  CodeInline,
38663
+ Column,
38658
38664
  Container,
38659
38665
  Heading,
38660
38666
  Hr,
38661
38667
  Img,
38662
38668
  Link,
38663
38669
  Preview,
38670
+ Row,
38671
+ Section,
38664
38672
  Text
38665
38673
  ];
38666
38674
  const isComponent = (element) => {
@@ -38702,10 +38710,10 @@ function cloneElementWithInlinedStyles(element, inlinableRules, nonInlinableRule
38702
38710
  const residualClasses = [];
38703
38711
  const rules = [];
38704
38712
  for (const className of classes) {
38705
- const rule = inlinableRules.get(className);
38706
- if (rule) rules.push(rule);
38713
+ const classRules = inlinableRules.get(className);
38714
+ if (classRules) rules.push(...classRules);
38707
38715
  if (nonInlinableRules.has(className)) residualClasses.push(className);
38708
- else if (!rule) residualClasses.push(className);
38716
+ else if (!classRules) residualClasses.push(className);
38709
38717
  }
38710
38718
  propsToOverwrite.style = {
38711
38719
  ...makeInlineStylesFor(rules, customProperties),
@@ -40642,7 +40650,7 @@ function Tailwind({ children, config, theme, utility }) {
40642
40650
  const customProperties = getCustomProperties(styleSheet);
40643
40651
  const nonInlineStyles = {
40644
40652
  type: "StyleSheet",
40645
- children: new List().fromArray(Array.from(nonInlinableRules.values()))
40653
+ children: new List().fromArray(Array.from(nonInlinableRules.values()).flat())
40646
40654
  };
40647
40655
  sanitizeNonInlinableRules(nonInlineStyles);
40648
40656
  downlevelForEmailClients(nonInlineStyles);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email",
3
- "version": "6.6.6",
3
+ "version": "6.6.8",
4
4
  "description": "A live preview of your emails right in your browser.",
5
5
  "bin": {
6
6
  "email": "./dist/cli/index.mjs"
@@ -131,6 +131,18 @@ console.log(\`Hello, $\{name}!\`);
131
131
  `);
132
132
  });
133
133
 
134
+ it('escapes double quotes in link/image href and title attributes', async () => {
135
+ const actualOutput = await render(
136
+ <Markdown>
137
+ {`[guide](https://example.com/?q="a" 'The "Complete" Guide') and ![logo](https://cdn.example.com/a.png 'Acme "logo"')`}
138
+ </Markdown>,
139
+ );
140
+ expect(actualOutput).toMatchInlineSnapshot(`
141
+ "<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><!--$--><div data-id="react-email-markdown"><p><a href="https://example.com/?q=&quot;a&quot;" target="_blank" title="The &quot;Complete&quot; Guide" style="color:#007bff;text-decoration:underline;background-color:transparent">guide</a> and <img src="https://cdn.example.com/a.png" alt="logo" title="Acme &quot;logo&quot;"></p>
142
+ </div><!--/$-->"
143
+ `);
144
+ });
145
+
134
146
  it('renders lists in the correct format for browsers', async () => {
135
147
  const actualOutput = await render(
136
148
  <Markdown>
@@ -98,7 +98,7 @@ export const Markdown = React.forwardRef<HTMLDivElement, MarkdownProps>(
98
98
 
99
99
  renderer.image = ({ href, text, title }) => {
100
100
  return `<img src="${href.replaceAll('"', '&quot;')}" alt="${text.replaceAll('"', '&quot;')}"${
101
- title ? ` title="${title}"` : ''
101
+ title ? ` title="${title.replaceAll('"', '&quot;')}"` : ''
102
102
  }${
103
103
  parseCssInJsToInlineCss(finalStyles.image) !== ''
104
104
  ? ` style="${parseCssInJsToInlineCss(finalStyles.image)}"`
@@ -109,8 +109,8 @@ export const Markdown = React.forwardRef<HTMLDivElement, MarkdownProps>(
109
109
  renderer.link = ({ href, title, tokens }) => {
110
110
  const text = renderer.parser.parseInline(tokens);
111
111
 
112
- return `<a href="${href}" target="_blank"${
113
- title ? ` title="${title}"` : ''
112
+ return `<a href="${href.replaceAll('"', '&quot;')}" target="_blank"${
113
+ title ? ` title="${title.replaceAll('"', '&quot;')}"` : ''
114
114
  }${
115
115
  parseCssInJsToInlineCss(finalStyles.link) !== ''
116
116
  ? ` style="${parseCssInJsToInlineCss(finalStyles.link)}"`
@@ -0,0 +1,49 @@
1
+ import { parse, type StyleSheet } from 'css-tree';
2
+ import { inlineStyles } from './inline-styles.js';
3
+
4
+ describe('inlineStyles()', () => {
5
+ it('inlines a single rule for a class', () => {
6
+ const styleSheet = parse(`
7
+ .box { border-radius: 8px; background-color: white; padding: 16px; }
8
+ `) as StyleSheet;
9
+
10
+ expect(inlineStyles(styleSheet, ['box'])).toMatchInlineSnapshot(`
11
+ {
12
+ "backgroundColor": "white",
13
+ "borderRadius": "8px",
14
+ "padding": "16px",
15
+ }
16
+ `);
17
+ });
18
+
19
+ it('merges declarations when the same class is defined across multiple rules', () => {
20
+ // Regression for #3628: a Tailwind preset and a child config both defining `.box`.
21
+ const styleSheet = parse(`
22
+ .box { border-radius: 8px; background-color: white; padding: 16px; }
23
+ .box { background-color: red; }
24
+ `) as StyleSheet;
25
+
26
+ expect(inlineStyles(styleSheet, ['box'])).toMatchInlineSnapshot(`
27
+ {
28
+ "backgroundColor": "red",
29
+ "borderRadius": "8px",
30
+ "padding": "16px",
31
+ }
32
+ `);
33
+ });
34
+
35
+ it("keeps a class's own cascade order when its rules are split by another class", () => {
36
+ const styleSheet = parse(`
37
+ .box { color: red; }
38
+ .other { font-weight: bold; }
39
+ .box { color: blue; }
40
+ `) as StyleSheet;
41
+
42
+ expect(inlineStyles(styleSheet, ['box', 'other'])).toMatchInlineSnapshot(`
43
+ {
44
+ "color": "blue",
45
+ "fontWeight": "bold",
46
+ }
47
+ `);
48
+ });
49
+ });
@@ -15,7 +15,7 @@ export function inlineStyles(
15
15
  const customProperties = getCustomProperties(styleSheet);
16
16
 
17
17
  return makeInlineStylesFor(
18
- Array.from(inlinableRules.values()),
18
+ Array.from(inlinableRules.values()).flat(),
19
19
  customProperties,
20
20
  );
21
21
  }
@@ -4,11 +4,14 @@ import React from 'react';
4
4
  import plugin from 'tailwindcss/plugin';
5
5
  import { Body } from '../body/index.js';
6
6
  import { Button } from '../button/index.js';
7
+ import { Column } from '../column/index.js';
7
8
  import { Head } from '../head/index.js';
8
9
  import { Heading } from '../heading/index.js';
9
10
  import { Hr } from '../hr/index.js';
10
11
  import { Html } from '../html/index.js';
11
12
  import { Link } from '../link/index.js';
13
+ import { Row } from '../row/index.js';
14
+ import { Section } from '../section/index.js';
12
15
  import type { TailwindConfig } from './tailwind.js';
13
16
  import { Tailwind } from './tailwind.js';
14
17
 
@@ -261,6 +264,45 @@ describe('Tailwind component', () => {
261
264
  );
262
265
  });
263
266
 
267
+ it('routes Tailwind padding on <Section> to the inner <td>', async () => {
268
+ const html = await render(
269
+ <Tailwind>
270
+ <Section className="bg-white p-4">x</Section>
271
+ </Tailwind>,
272
+ );
273
+
274
+ expect(html).toContain('<td style="padding:1rem">');
275
+ expect(html).not.toMatch(/<table[^>]*style="[^"]*padding:1rem/);
276
+ });
277
+
278
+ it('inlines Tailwind classes on <Column> onto its <td>', async () => {
279
+ const html = await render(
280
+ <Tailwind>
281
+ <Row>
282
+ <Column className="bg-white p-4">x</Column>
283
+ </Row>
284
+ </Tailwind>,
285
+ );
286
+
287
+ expect(html).toMatch(
288
+ /<td[^>]*data-id="__react-email-column"[^>]*style="[^"]*padding:1rem/,
289
+ );
290
+ });
291
+
292
+ it('inlines Tailwind classes on <Row> onto its <table>', async () => {
293
+ const html = await render(
294
+ <Tailwind>
295
+ <Row className="bg-white p-4">
296
+ <Column>x</Column>
297
+ </Row>
298
+ </Tailwind>,
299
+ );
300
+
301
+ expect(html).toMatch(
302
+ /<table[^>]*role="presentation"[^>]*style="[^"]*padding:1rem/,
303
+ );
304
+ });
305
+
264
306
  it('works with components that use React.forwardRef', async () => {
265
307
  const Wrapper = (props: { children: React.ReactNode }) => {
266
308
  return <Tailwind>{props.children}</Tailwind>;
@@ -742,6 +784,42 @@ describe('Tailwind component', () => {
742
784
  });
743
785
  });
744
786
 
787
+ describe('with duplicate classes across presets and plugins', () => {
788
+ it('merges declarations from a preset and a child override for the same class', async () => {
789
+ const base: TailwindConfig = {
790
+ plugins: [
791
+ plugin(({ addComponents }) => {
792
+ addComponents({ '.box': { '@apply rounded-lg bg-white p-4': {} } });
793
+ }),
794
+ ],
795
+ };
796
+ const config: TailwindConfig = {
797
+ presets: [base],
798
+ plugins: [
799
+ plugin(({ addComponents }) => {
800
+ addComponents({ '.box': { '@apply bg-red-500': {} } });
801
+ }),
802
+ ],
803
+ };
804
+
805
+ const actualOutput = await render(
806
+ <Tailwind config={config}>
807
+ <div className="box">hi</div>
808
+ </Tailwind>,
809
+ ).then(pretty);
810
+
811
+ expect(actualOutput).toMatchInlineSnapshot(`
812
+ "<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
813
+ <!--$-->
814
+ <div style="border-radius:0.5rem;background-color:rgb(251,44,54);padding:1rem">
815
+ hi
816
+ </div>
817
+ <!--/$-->
818
+ "
819
+ `);
820
+ });
821
+ });
822
+
745
823
  describe('with custom theme config', () => {
746
824
  it('supports custom colors', async () => {
747
825
  const config: TailwindConfig = {
@@ -1102,7 +1180,7 @@ describe('Tailwind component', () => {
1102
1180
  "<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1103
1181
  <!--$-->
1104
1182
  <div
1105
- style="box-shadow:0 4px 6px rgba(0,0,0,0.1);border-radius:8px;padding:16px"></div>
1183
+ style="box-shadow:0 4px 6px rgb(0,0,0,0.1);border-radius:8px;padding:16px"></div>
1106
1184
  <!--/$-->
1107
1185
  "
1108
1186
  `);
@@ -124,7 +124,7 @@ export function Tailwind({ children, config, theme, utility }: TailwindProps) {
124
124
  const nonInlineStyles: StyleSheet = {
125
125
  type: 'StyleSheet',
126
126
  children: new List<CssNode>().fromArray(
127
- Array.from(nonInlinableRules.values()),
127
+ Array.from(nonInlinableRules.values()).flat(),
128
128
  ),
129
129
  };
130
130
  sanitizeNonInlinableRules(nonInlineStyles);
@@ -3,8 +3,14 @@ import { setupTailwind } from '../tailwindcss/setup-tailwind.js';
3
3
  import { extractRulesPerClass } from './extract-rules-per-class.js';
4
4
 
5
5
  describe('extractRulesPerClass()', async () => {
6
- function convertToComparable(map: Map<string, Rule>): Record<string, string> {
7
- return Object.fromEntries(map.entries().map(([k, v]) => [k, generate(v)]));
6
+ function convertToComparable(
7
+ map: Map<string, Rule[]>,
8
+ ): Record<string, string[]> {
9
+ return Object.fromEntries(
10
+ map
11
+ .entries()
12
+ .map(([k, rules]) => [k, rules.map((rule) => generate(rule))]),
13
+ );
8
14
  }
9
15
 
10
16
  it('works with just inlinable utilities', async () => {
@@ -21,13 +27,50 @@ describe('extractRulesPerClass()', async () => {
21
27
 
22
28
  expect(convertToComparable(inlinable)).toMatchInlineSnapshot(`
23
29
  {
24
- "bg-red-500": ".bg-red-500{background-color:var(--color-red-500)}",
25
- "text-center": ".text-center{text-align:center}",
30
+ "bg-red-500": [
31
+ ".bg-red-500{background-color:var(--color-red-500)}",
32
+ ],
33
+ "text-center": [
34
+ ".text-center{text-align:center}",
35
+ ],
26
36
  }
27
37
  `);
28
38
  expect(convertToComparable(nonInlinable)).toMatchInlineSnapshot('{}');
29
39
  });
30
40
 
41
+ it('keeps every rule when a class is defined more than once', async () => {
42
+ const tailwind = await setupTailwind({
43
+ config: {
44
+ plugins: [
45
+ {
46
+ handler: (api) => {
47
+ api.addComponents({
48
+ '.box': { '@apply rounded-lg bg-white p-4': {} },
49
+ });
50
+ api.addComponents({
51
+ '.box': { '@apply bg-red-500': {} },
52
+ });
53
+ },
54
+ },
55
+ ],
56
+ },
57
+ });
58
+ const classes = ['box'];
59
+ tailwind.addUtilities(classes);
60
+
61
+ const stylesheet = tailwind.getStyleSheet();
62
+ const { inlinable } = extractRulesPerClass(stylesheet, classes);
63
+
64
+ expect(convertToComparable(inlinable)).toMatchInlineSnapshot(`
65
+ {
66
+ "box": [
67
+ ".box{border-radius:var(--radius-lg);background-color:var(--color-white);padding:calc(var(--spacing)*4)}",
68
+ ".box{background-color:var(--color-red-500)}",
69
+ ],
70
+ }
71
+ `);
72
+ });
73
+
31
74
  it('handles non-inlinable utilities', async () => {
32
75
  const tailwind = await setupTailwind({});
33
76
  const classes = ['lg:w-1/2'];
@@ -43,7 +86,9 @@ describe('extractRulesPerClass()', async () => {
43
86
  expect(convertToComparable(inlinable)).toMatchInlineSnapshot('{}');
44
87
  expect(convertToComparable(nonInlinable)).toMatchInlineSnapshot(`
45
88
  {
46
- "lg:w-1/2": ".lg\\:w-1\\/2{@media (width>=64rem){width:calc(1/2*100%)}}",
89
+ "lg:w-1/2": [
90
+ ".lg\\:w-1\\/2{@media (width>=64rem){width:calc(1/2*100%)}}",
91
+ ],
47
92
  }
48
93
  `);
49
94
  });
@@ -66,14 +111,22 @@ describe('extractRulesPerClass()', async () => {
66
111
  );
67
112
  expect(convertToComparable(inlinable)).toMatchInlineSnapshot(`
68
113
  {
69
- "bg-red-500": ".bg-red-500{background-color:var(--color-red-500)}",
70
- "text-center": ".text-center{text-align:center}",
71
- "w-full": ".w-full{width:100%}",
114
+ "bg-red-500": [
115
+ ".bg-red-500{background-color:var(--color-red-500)}",
116
+ ],
117
+ "text-center": [
118
+ ".text-center{text-align:center}",
119
+ ],
120
+ "w-full": [
121
+ ".w-full{width:100%}",
122
+ ],
72
123
  }
73
124
  `);
74
125
  expect(convertToComparable(nonInlinable)).toMatchInlineSnapshot(`
75
126
  {
76
- "lg:w-1/2": ".lg\\:w-1\\/2{@media (width>=64rem){width:calc(1/2*100%)}}",
127
+ "lg:w-1/2": [
128
+ ".lg\\:w-1\\/2{@media (width>=64rem){width:calc(1/2*100%)}}",
129
+ ],
77
130
  }
78
131
  `);
79
132
  });
@@ -105,12 +158,16 @@ describe('extractRulesPerClass()', async () => {
105
158
 
106
159
  expect(convertToComparable(inlinable)).toMatchInlineSnapshot(`
107
160
  {
108
- "text-body": ".text-body{color:green}",
161
+ "text-body": [
162
+ ".text-body{color:green}",
163
+ ],
109
164
  }
110
165
  `);
111
166
  expect(convertToComparable(nonInlinable)).toMatchInlineSnapshot(`
112
167
  {
113
- "text-body": ".text-body{@media (width>=40rem){color:darkgreen}}",
168
+ "text-body": [
169
+ ".text-body{@media (width>=40rem){color:darkgreen}}",
170
+ ],
114
171
  }
115
172
  `);
116
173
  });
@@ -143,7 +200,9 @@ describe('extractRulesPerClass()', async () => {
143
200
  expect(convertToComparable(inlinable)).toMatchInlineSnapshot('{}');
144
201
  expect(convertToComparable(nonInlinable)).toMatchInlineSnapshot(`
145
202
  {
146
- "btn": ".btn{&:hover{color:red}}",
203
+ "btn": [
204
+ ".btn{&:hover{color:red}}",
205
+ ],
147
206
  }
148
207
  `);
149
208
  });
@@ -5,8 +5,24 @@ import { splitMixedRule } from './split-mixed-rule.js';
5
5
  export function extractRulesPerClass(root: CssNode, classes: string[]) {
6
6
  const classSet = new Set(classes);
7
7
 
8
- const inlinableRules = new Map<string, Rule>();
9
- const nonInlinableRules = new Map<string, Rule>();
8
+ // A class can be defined by multiple rules (e.g. a preset and a child config
9
+ // override), so keep them all to merge instead of the last one clobbering.
10
+ const inlinableRules = new Map<string, Rule[]>();
11
+ const nonInlinableRules = new Map<string, Rule[]>();
12
+
13
+ const appendRule = (
14
+ map: Map<string, Rule[]>,
15
+ className: string,
16
+ rule: Rule,
17
+ ) => {
18
+ const existing = map.get(className);
19
+ if (existing) {
20
+ existing.push(rule);
21
+ } else {
22
+ map.set(className, [rule]);
23
+ }
24
+ };
25
+
10
26
  walk(root, {
11
27
  visit: 'Rule',
12
28
  enter(rule) {
@@ -20,7 +36,7 @@ export function extractRulesPerClass(root: CssNode, classes: string[]) {
20
36
  if (isRuleInlinable(rule)) {
21
37
  for (const className of selectorClasses) {
22
38
  if (classSet.has(className)) {
23
- inlinableRules.set(className, rule);
39
+ appendRule(inlinableRules, className, rule);
24
40
  }
25
41
  }
26
42
  } else {
@@ -28,10 +44,10 @@ export function extractRulesPerClass(root: CssNode, classes: string[]) {
28
44
  for (const className of selectorClasses) {
29
45
  if (!classSet.has(className)) continue;
30
46
  if (inlinablePart) {
31
- inlinableRules.set(className, inlinablePart);
47
+ appendRule(inlinableRules, className, inlinablePart);
32
48
  }
33
49
  if (nonInlinablePart) {
34
- nonInlinableRules.set(className, nonInlinablePart);
50
+ appendRule(nonInlinableRules, className, nonInlinablePart);
35
51
  }
36
52
  }
37
53
  }
@@ -129,6 +129,26 @@ describe('sanitizeDeclarations', () => {
129
129
  );
130
130
  });
131
131
 
132
+ it('separates two-value logical shorthands with auto, var, or calc', () => {
133
+ let root = parse('.x { margin-inline: 1rem auto; }');
134
+ sanitizeDeclarations(root);
135
+ expect(generate(root)).toMatchInlineSnapshot(
136
+ `".x{margin-right:auto;margin-left:1rem}"`,
137
+ );
138
+
139
+ root = parse('.x { margin-inline: 0 auto; }');
140
+ sanitizeDeclarations(root);
141
+ expect(generate(root)).toMatchInlineSnapshot(
142
+ `".x{margin-right:auto;margin-left:0}"`,
143
+ );
144
+
145
+ root = parse('.x { padding-inline: 10px calc(1rem + 2px); }');
146
+ sanitizeDeclarations(root);
147
+ expect(generate(root)).toMatchInlineSnapshot(
148
+ `".x{padding-right:calc(1rem + 2px);padding-left:10px}"`,
149
+ );
150
+ });
151
+
132
152
  test('oklch to rgb conversion', () => {
133
153
  let stylesheet = parse('div { color: oklch(90.5% 0.2 180); }');
134
154
  sanitizeDeclarations(stylesheet);
@@ -230,6 +250,27 @@ describe('sanitizeDeclarations', () => {
230
250
  generate(stylesheet),
231
251
  'treatment for already supported rgb syntax',
232
252
  ).toMatchInlineSnapshot(`"div{color:rgb(255,0,128)}"`);
253
+
254
+ stylesheet = parse('div { color: rgba(255 0 128 / 0.5); }');
255
+ sanitizeDeclarations(stylesheet);
256
+ expect(
257
+ generate(stylesheet),
258
+ 'rgba() space syntax with alpha',
259
+ ).toMatchInlineSnapshot(`"div{color:rgb(255,0,128,0.5)}"`);
260
+
261
+ stylesheet = parse('div { color: rgba(100% 0% 50% / 100%); }');
262
+ sanitizeDeclarations(stylesheet);
263
+ expect(
264
+ generate(stylesheet),
265
+ 'rgba() percentage syntax with full alpha',
266
+ ).toMatchInlineSnapshot(`"div{color:rgb(255,0,128)}"`);
267
+
268
+ stylesheet = parse('div { color: rgba(255, 0, 128, 0.5); }');
269
+ sanitizeDeclarations(stylesheet);
270
+ expect(
271
+ generate(stylesheet),
272
+ 'legacy comma rgba() is left untouched',
273
+ ).toMatchInlineSnapshot(`"div{color:rgb(255,0,128,0.5)}"`);
233
274
  });
234
275
 
235
276
  test('hex to rgb conversion', () => {
@@ -133,11 +133,11 @@ function separteShorthandDeclaration(
133
133
  shorthandToReplace.value.type === 'Value'
134
134
  ? shorthandToReplace.value.children
135
135
  .toArray()
136
+ // Count every real value component, not just numeric ones, so a
137
+ // two-value shorthand with `auto`/`var()`/`calc()` (e.g.
138
+ // `margin-inline: 1rem auto`) still splits into two longhands.
136
139
  .filter(
137
- (child) =>
138
- child.type === 'Dimension' ||
139
- child.type === 'Number' ||
140
- child.type === 'Percentage',
140
+ (child) => child.type !== 'Operator' && child.type !== 'WhiteSpace',
141
141
  )
142
142
  : [shorthandToReplace.value];
143
143
  let endValue = shorthandToReplace.value;
@@ -252,7 +252,7 @@ export function sanitizeDeclarations(nodeContainingDeclarations: CssNode) {
252
252
  funcParentListItem.data = rgbNode(rgb.r, rgb.g, rgb.b, a);
253
253
  }
254
254
 
255
- if (func.name === 'rgb') {
255
+ if (func.name === 'rgb' || func.name === 'rgba') {
256
256
  let r: number | undefined;
257
257
  let g: number | undefined;
258
258
  let b: number | undefined;
@@ -2,12 +2,15 @@ import { Body } from '../../../body/index.js';
2
2
  import { Button } from '../../../button/index.js';
3
3
  import { CodeBlock } from '../../../code-block/index.js';
4
4
  import { CodeInline } from '../../../code-inline/index.js';
5
+ import { Column } from '../../../column/index.js';
5
6
  import { Container } from '../../../container/index.js';
6
7
  import { Heading } from '../../../heading/index.js';
7
8
  import { Hr } from '../../../hr/index.js';
8
9
  import { Img } from '../../../img/index.js';
9
10
  import { Link } from '../../../link/index.js';
10
11
  import { Preview } from '../../../preview/index.js';
12
+ import { Row } from '../../../row/index.js';
13
+ import { Section } from '../../../section/index.js';
11
14
  import { Text } from '../../../text/index.js';
12
15
 
13
16
  const componentsToTreatAsElements: React.ReactElement['type'][] = [
@@ -15,12 +18,15 @@ const componentsToTreatAsElements: React.ReactElement['type'][] = [
15
18
  Button,
16
19
  CodeBlock,
17
20
  CodeInline,
21
+ Column,
18
22
  Container,
19
23
  Heading,
20
24
  Hr,
21
25
  Img,
22
26
  Link,
23
27
  Preview,
28
+ Row,
29
+ Section,
24
30
  Text,
25
31
  ];
26
32
 
@@ -8,8 +8,8 @@ import { isComponent } from '../react/is-component.js';
8
8
 
9
9
  export function cloneElementWithInlinedStyles(
10
10
  element: React.ReactElement<EmailElementProps>,
11
- inlinableRules: Map<string, Rule>,
12
- nonInlinableRules: Map<string, Rule>,
11
+ inlinableRules: Map<string, Rule[]>,
12
+ nonInlinableRules: Map<string, Rule[]>,
13
13
  customProperties: CustomProperties,
14
14
  ) {
15
15
  const propsToOverwrite: Partial<EmailElementProps> = {};
@@ -21,13 +21,13 @@ export function cloneElementWithInlinedStyles(
21
21
 
22
22
  const rules: Rule[] = [];
23
23
  for (const className of classes) {
24
- const rule = inlinableRules.get(className);
25
- if (rule) {
26
- rules.push(rule);
24
+ const classRules = inlinableRules.get(className);
25
+ if (classRules) {
26
+ rules.push(...classRules);
27
27
  }
28
28
  if (nonInlinableRules.has(className)) {
29
29
  residualClasses.push(className);
30
- } else if (!rule) {
30
+ } else if (!classRules) {
31
31
  residualClasses.push(className);
32
32
  }
33
33
  }