@react-email/tailwind
Advanced tools
Comparing version
@@ -57,24 +57,151 @@ "use strict"; | ||
var import_server = require("react-dom/server"); | ||
var import_html_react_parser = __toESM(require("html-react-parser")); | ||
var import_tw_to_css = require("tw-to-css"); | ||
// src/utils/css-to-jsx-style.ts | ||
var camelCase = (string) => string.replace(/-(\w|$)/g, (_, p1) => p1.toUpperCase()); | ||
var convertPropertyName = (prop) => { | ||
let modifiedProp = prop; | ||
modifiedProp = modifiedProp.toLowerCase(); | ||
if (modifiedProp === "float") { | ||
return "cssFloat"; | ||
} | ||
if (modifiedProp.startsWith("--")) { | ||
return modifiedProp; | ||
} | ||
if (modifiedProp.startsWith("-ms-")) { | ||
modifiedProp = modifiedProp.substr(1); | ||
} | ||
return camelCase(modifiedProp); | ||
}; | ||
var splitDeclarations = (cssText) => { | ||
const declarations = []; | ||
let capturing; | ||
let i = cssText.length; | ||
let last = i; | ||
while (i-- > -1) { | ||
if ((cssText[i] === '"' || cssText[i] === "'") && cssText[i - 1] !== "\\") { | ||
if (!capturing) { | ||
capturing = cssText[i]; | ||
} else if (cssText[i] === capturing) { | ||
capturing = false; | ||
} | ||
} | ||
if (!capturing && cssText[i] === ")") { | ||
capturing = cssText[i]; | ||
} | ||
if (cssText[i] === "(" && capturing === ")") { | ||
capturing = false; | ||
} | ||
if (i < 0 || !capturing && cssText[i] === ";") { | ||
declarations.unshift(cssText.slice(i + 1, last)); | ||
last = i; | ||
} | ||
} | ||
return declarations; | ||
}; | ||
var splitDeclaration = (declaration) => { | ||
const i = declaration.indexOf(":"); | ||
return [declaration.substr(0, i).trim(), declaration.substr(i + 1).trim()]; | ||
}; | ||
var cssToJsxStyle = (cssText) => splitDeclarations(cssText).map(splitDeclaration).reduce((styles, [name, value]) => { | ||
if (name && value) { | ||
styles[convertPropertyName(name)] = value; | ||
} | ||
return styles; | ||
}, {}); | ||
// src/tailwind.tsx | ||
var import_jsx_runtime = require("react/jsx-runtime"); | ||
function processElement(element, headStyles, twi) { | ||
let modifiedElement = element; | ||
if (modifiedElement.props.className) { | ||
const convertedStyles = []; | ||
const responsiveStyles = []; | ||
const classNames = modifiedElement.props.className.split(" "); | ||
const customClassNames = classNames.filter((className) => { | ||
const tailwindClassName = twi(className, { ignoreMediaQueries: true }); | ||
if (tailwindClassName) { | ||
convertedStyles.push(tailwindClassName); | ||
return false; | ||
} else if (twi(className, { ignoreMediaQueries: false })) { | ||
responsiveStyles.push(className); | ||
return false; | ||
} | ||
return true; | ||
}); | ||
const convertedResponsiveStyles = twi(responsiveStyles, { | ||
ignoreMediaQueries: false, | ||
merge: false | ||
}); | ||
headStyles.push( | ||
convertedResponsiveStyles.replace(/^\n+/, "").replace(/\n+$/, "") | ||
); | ||
modifiedElement = React.cloneElement(modifiedElement, __spreadProps(__spreadValues({}, modifiedElement.props), { | ||
className: customClassNames.length ? customClassNames.join(" ") : void 0, | ||
style: __spreadValues(__spreadValues({}, modifiedElement.props.style), cssToJsxStyle(convertedStyles.join(" "))) | ||
})); | ||
} | ||
if (modifiedElement.props.children) { | ||
const children = React.Children.toArray(modifiedElement.props.children); | ||
const processedChildren = children.map((child) => { | ||
if (React.isValidElement(child)) { | ||
return processElement(child, headStyles, twi); | ||
} | ||
return child; | ||
}); | ||
modifiedElement = React.cloneElement( | ||
modifiedElement, | ||
modifiedElement.props, | ||
...processedChildren | ||
); | ||
} | ||
return modifiedElement; | ||
} | ||
function processHead(child, responsiveStyles) { | ||
let modifiedChild = child; | ||
if (modifiedChild.type === "head" || modifiedChild.type.displayName === "Head") { | ||
const styleElement = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: responsiveStyles }); | ||
const headChildren = React.Children.toArray(modifiedChild.props.children); | ||
headChildren.push(styleElement); | ||
modifiedChild = React.cloneElement( | ||
modifiedChild, | ||
modifiedChild.props, | ||
...headChildren | ||
); | ||
} | ||
if (modifiedChild.props.children) { | ||
const children = React.Children.toArray(modifiedChild.props.children); | ||
const processedChildren = children.map((processedChild) => { | ||
if (React.isValidElement(processedChild)) { | ||
return processHead(processedChild, responsiveStyles); | ||
} | ||
return processedChild; | ||
}); | ||
modifiedChild = React.cloneElement( | ||
modifiedChild, | ||
modifiedChild.props, | ||
...processedChildren | ||
); | ||
} | ||
return modifiedChild; | ||
} | ||
var Tailwind = ({ children, config }) => { | ||
const headStyles = []; | ||
const { twi } = (0, import_tw_to_css.tailwindToCSS)({ | ||
config | ||
}); | ||
const newChildren = React.Children.toArray(children); | ||
const fullHTML = (0, import_server.renderToStaticMarkup)(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: newChildren })); | ||
const tailwindCss = twi(fullHTML, { | ||
merge: false, | ||
ignoreMediaQueries: false | ||
const childrenWithInlineStyles = React.Children.map(children, (child) => { | ||
if (React.isValidElement(child)) { | ||
return processElement(child, headStyles, twi); | ||
} | ||
return child; | ||
}); | ||
const css = cleanCss(tailwindCss); | ||
const cssMap = makeCssMap(css); | ||
const headStyle = getMediaQueryCss(css); | ||
if (!childrenWithInlineStyles) | ||
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children }); | ||
const fullHTML = (0, import_server.renderToStaticMarkup)(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: childrenWithInlineStyles })); | ||
const hasResponsiveStyles = new RegExp("@media[^{]+\\{(?<content>[\\s\\S]+?)\\}\\s*\\}", "gm").test( | ||
headStyle | ||
headStyles.join(" ") | ||
); | ||
const hasHTML = /<html[^>]*>/gm.test(fullHTML); | ||
const hasHead = /<head[^>]*>/gm.test(fullHTML); | ||
if (hasResponsiveStyles && (!hasHTML || !hasHead)) { | ||
const hasHTMLAndHead = /<html[^>]*>(?=[\s\S]*<head[^>]*>)/gm.test(fullHTML); | ||
if (hasResponsiveStyles && !hasHTMLAndHead) { | ||
throw new Error( | ||
@@ -84,87 +211,13 @@ "Tailwind: To use responsive styles you must have a <html> and <head> element in your template." | ||
} | ||
const reactHTML = React.Children.map(newChildren, (child) => { | ||
if (!React.isValidElement(child)) | ||
const childrenWithInlineAndResponsiveStyles = React.Children.map( | ||
childrenWithInlineStyles, | ||
(child) => { | ||
if (React.isValidElement(child)) { | ||
return processHead(child, headStyles); | ||
} | ||
return child; | ||
const html = (0, import_server.renderToStaticMarkup)(child); | ||
const parsedHTML = (0, import_html_react_parser.default)(html, { | ||
replace: (domNode) => { | ||
var _a; | ||
if (domNode instanceof import_html_react_parser.Element) { | ||
if (hasResponsiveStyles && hasHead && domNode.name === "head") { | ||
let newDomNode = null; | ||
if (domNode.children) { | ||
const props = (0, import_html_react_parser.attributesToProps)(domNode.attribs); | ||
newDomNode = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("head", __spreadProps(__spreadValues({}, props), { children: [ | ||
(0, import_html_react_parser.domToReact)(domNode.children), | ||
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: headStyle }) | ||
] })); | ||
} | ||
return newDomNode; | ||
} | ||
if ((_a = domNode.attribs) == null ? void 0 : _a.class) { | ||
const cleanRegex = /[:#\!\-[\]\/\.%]+/g; | ||
const cleanTailwindClasses = domNode.attribs.class.replace(cleanRegex, "_"); | ||
const currentStyles = domNode.attribs.style ? `${domNode.attribs.style};` : ""; | ||
const tailwindStyles = cleanTailwindClasses.split(" ").map((className) => { | ||
return cssMap[`.${className}`]; | ||
}).join(";"); | ||
domNode.attribs.style = `${currentStyles} ${tailwindStyles}`; | ||
domNode.attribs.class = domNode.attribs.class.split(" ").filter((className) => { | ||
const cleanedClassName = className.replace(cleanRegex, "_"); | ||
return className.search(/^.{2}:/) !== -1 || !cssMap[`.${cleanedClassName}`]; | ||
}).join(" ").replace(cleanRegex, "_"); | ||
if (domNode.attribs.class === "") | ||
delete domNode.attribs.class; | ||
} | ||
} | ||
} | ||
}); | ||
return parsedHTML; | ||
}); | ||
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: reactHTML }); | ||
} | ||
); | ||
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: childrenWithInlineAndResponsiveStyles }); | ||
}; | ||
Tailwind.displayName = "Tailwind"; | ||
function cleanCss(css) { | ||
let newCss = css.replace(/\\/g, "").replace(/[.\!\#\w\d\\:\-\[\]\/\.%\(\))]+(?=\s*?{[^{]*?\})\s*?{/g, (m) => { | ||
return m.replace(new RegExp("(?<=.)[:#\\!\\-[\\\\\\]\\/\\.%]+", "g"), "_"); | ||
}).replace(new RegExp("font-family(?<value>[^;\\r\\n]+)", "g"), (m, value) => { | ||
return `font-family${value.replace(/['"]+/g, "")}`; | ||
}); | ||
return newCss; | ||
} | ||
function getMediaQueryCss(css) { | ||
var _a, _b; | ||
const mediaQueryRegex = new RegExp("@media[^{]+\\{(?<content>[\\s\\S]+?)\\}\\s*\\}", "gm"); | ||
return (_b = (_a = css.replace(mediaQueryRegex, (m) => { | ||
return m.replace( | ||
/([^{]+\{)([\s\S]+?)(\}\s*\})/gm, | ||
(_, start, content, end) => { | ||
const newContent = content.replace( | ||
new RegExp("(?:[\\s\\r\\n]*)?(?<prop>[\\w-]+)\\s*:\\s*(?<value>[^};\\r\\n]+)", "gm"), | ||
(_2, prop, value) => { | ||
return `${prop}: ${value} !important;`; | ||
} | ||
); | ||
return `${start}${newContent}${end}`; | ||
} | ||
); | ||
}).match(/@media\s*([^{]+)\{([^{}]*\{[^{}]*\})*[^{}]*\}/g)) == null ? void 0 : _a.join("")) != null ? _b : ""; | ||
} | ||
function makeCssMap(css) { | ||
const cssNoMedia = css.replace( | ||
new RegExp("@media[^{]+\\{(?<content>[\\s\\S]+?)\\}\\s*\\}", "gm"), | ||
"" | ||
); | ||
const cssMap = cssNoMedia.split("}").reduce( | ||
(acc, cur) => { | ||
const [key, value] = cur.split("{"); | ||
if (key && value) { | ||
acc[key] = value; | ||
} | ||
return acc; | ||
}, | ||
{} | ||
); | ||
return cssMap; | ||
} | ||
// Annotate the CommonJS export names for ESM import in node: | ||
@@ -171,0 +224,0 @@ 0 && (module.exports = { |
{ | ||
"name": "@react-email/tailwind", | ||
"version": "0.0.9", | ||
"version": "0.0.11-canary.0", | ||
"description": "A React component to wrap emails with Tailwind CSS", | ||
@@ -27,9 +27,7 @@ "sideEffects": false, | ||
"build": "tsup src/index.ts --format esm,cjs --dts --external react", | ||
"clean": "rm -rf dist", | ||
"dev": "tsup src/index.ts --format esm,cjs --dts --external react --watch", | ||
"lint": "eslint", | ||
"clean": "rm -rf dist", | ||
"test": "jest", | ||
"test:watch": "jest --watch", | ||
"format:check": "prettier --check \"**/*.{ts,tsx,md}\"", | ||
"format": "prettier --write \"**/*.{ts,tsx,md}\"" | ||
"lint": "eslint .", | ||
"test:watch": "vitest", | ||
"test": "vitest run" | ||
}, | ||
@@ -47,25 +45,21 @@ "repository": { | ||
"engines": { | ||
"node": ">=16.0.0" | ||
"node": ">=18.0.0" | ||
}, | ||
"dependencies": { | ||
"html-react-parser": "4.0.0", | ||
"react": "18.2.0", | ||
"react-dom": "18.2.0", | ||
"tw-to-css": "0.0.11" | ||
"tw-to-css": "0.0.12" | ||
}, | ||
"peerDependencies": { | ||
"react": "18.2.0" | ||
}, | ||
"devDependencies": { | ||
"@babel/core": "^7.21.8", | ||
"@babel/core": "7.21.8", | ||
"@babel/preset-react": "7.22.5", | ||
"@react-email/button": "0.0.7", | ||
"@react-email/hr": "workspace:*", | ||
"@react-email/head": "workspace:*", | ||
"@react-email/html": "workspace:*", | ||
"@testing-library/react": "14.0.0", | ||
"@types/jest": "29.5.3", | ||
"@types/react": "18.0.20", | ||
"@types/react-dom": "18.0.6", | ||
"babel-jest": "29.6.1", | ||
"eslint": "8.45.0", | ||
"jest": "29.6.1", | ||
"prettier": "3.0.0", | ||
"react": "18.2.0", | ||
"ts-jest": "29.1.1", | ||
"tsup": "7.1.0", | ||
"eslint-config-custom": "workspace:*", | ||
"tsconfig": "workspace:*", | ||
"typescript": "5.1.6" | ||
@@ -72,0 +66,0 @@ }, |
Sorry, the diff of this file is not supported yet
21223
10.2%9
-40%415
30.91%20
-4.76%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated