diff --git a/.changeset/fast-bobcats-battle.md b/.changeset/fast-bobcats-battle.md
new file mode 100644
index 0000000..261c903
--- /dev/null
+++ b/.changeset/fast-bobcats-battle.md
@@ -0,0 +1,13 @@
+---
+"md-to-react-email": major
+---
+
+### Changes
+
+- Added [`Marked`](https://marked.js.org/) for markdown transformations
+- Removed `ParseMarkdownToReactEmail` function
+
+### Fixes
+
+- Fixed issue with [list parsing](https://github.com/codeskills-dev/md-to-react-email/issues/11)
+- Fixed `parseCssInJsToInlineCss` issue with numerical values
diff --git a/README.md b/README.md
index 29a9861..b5ff42b 100644
--- a/README.md
+++ b/README.md
@@ -6,12 +6,13 @@ Read the documentation [here](https://md2re.codeskills.dev/)
md-to-react-email is a lightweight utility for converting [Markdown](https://www.markdownguide.org/) into valid [React-email](https://react.email) templates. This tool simplifies the process of creating responsive and customizable email templates by leveraging the power of React and Markdown.
+**Note**: Starting from `version 4`, `md-to-react-email` uses [`Marked`](https://marked.js.org/) for markdown transformation. see all changes [here](/CHANGELOG.md)
+
### Support
The following markdown flavors are supported
- Offical markdown flavour
-- Github flavoured markdown
## Installation
@@ -35,7 +36,6 @@ npm install md-to-react-email
- `camelToKebabCase`: converts strings from camelcase ['thisIsCamelCase'] to kebab case ['this-is-kebab-case']
- `parseCssInJsToInlineCss`: converts css styles from css-in-js to inline css e.g fontSize: "18px" => font-size: 18px;
-- `parseMarkdownToReactEmail`: parses markdown to a valid react-email string that can be copied and pasted directly into your codebase
- `parseMarkdownToReactEmailJSX`: parses markdown to valid react-email JSX for the client (i.e the browser)
### Components:
@@ -75,18 +75,6 @@ npm install md-to-react-email
```
-- For code generation (copy and paste)
-
- ```
- import {parseMarkdownToReactEmail} from "md-to-react-email"
-
- const markdown = `# Hello World`
- const parsedReactMail = parseMarkdownToReactEmail(markdown)
-
- console.log(parsedReactMail) // ` `
-
- ```
-
## Components
md-to-react-email contains pre-defined react-email and html components for the email template structure and styling. You can modify these components to customize the look and feel of your email template.
@@ -98,7 +86,7 @@ The following components are available for customization:
- Text: paragraphs, bold and italic text
- Links
- Code: Code blocks and inline code
-- Lists: ul, li
+- Lists: ul, ol, li
- Image
- Line-breaks (br)
- Horizontal-rule (hr)
diff --git a/__tests__/camelToKebabCase.test.ts b/__tests__/camelToKebabCase.test.ts
index c1d0f02..83e46e6 100644
--- a/__tests__/camelToKebabCase.test.ts
+++ b/__tests__/camelToKebabCase.test.ts
@@ -1,4 +1,4 @@
-import { camelToKebabCase } from "../src";
+import { camelToKebabCase } from "../src/utils";
describe("camelToKebabCase", () => {
it("should convert camel case to kebab case", () => {
diff --git a/__tests__/parseCssInJsToInlineCss.test.ts b/__tests__/parseCssInJsToInlineCss.test.ts
index 08e3cf0..ce8447e 100644
--- a/__tests__/parseCssInJsToInlineCss.test.ts
+++ b/__tests__/parseCssInJsToInlineCss.test.ts
@@ -1,4 +1,4 @@
-import { parseCssInJsToInlineCss } from "../src";
+import { parseCssInJsToInlineCss } from "../src/utils";
describe("parseCssInJsToInlineCss", () => {
test("should return an empty string for undefined CSS properties", () => {
diff --git a/__tests__/parseMarkdownToReactEmail.test.ts b/__tests__/parseMarkdownToReactEmail.test.ts
deleted file mode 100644
index 6d50594..0000000
--- a/__tests__/parseMarkdownToReactEmail.test.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-import { parseMarkdownToReactEmail } from "../src";
-
-describe("Markdown to React Mail Parser", () => {
- it("converts headers correctly", () => {
- const markdown = "# Hello, World!";
- const expected = ``;
-
- const rendered = parseMarkdownToReactEmail(markdown);
- expect(rendered).toBe(expected);
- });
-
- it("converts paragraphs, headers, lists, and tables correctly", () => {
- const markdown = `# Header 1
-
-This is a paragraph with some text. You can have multiple paragraphs separated by empty lines.
-
-## Header 2
-
-Here is an unordered list:
-- Item 1
-- Item 2
-- Item 3
-
-### Header 3
-
-Here is an ordered list:
-1. First
-2. Second
-3. Third
-
-#### Header 4
-
-A table example:
-
-| Column 1 | Column 2 |
-|----------|----------|
-| Cell 1 | Cell 2 |
-| Cell 3 | Cell 4 |
-`;
-
- const expected = `Header 1
-
-This is a paragraph with some text. You can have multiple paragraphs separated by empty lines.
-
-Header 2
-
-Here is an unordered list:
-
-
-Header 3
-
-Here is an ordered list:
-First
-Second
-Third
-
-Header 4
-
-A table example:
-Column 1 Column 2 Cell 1 Cell 2 Cell 3 Cell 4
`;
-
- const rendered = parseMarkdownToReactEmail(markdown);
- expect(rendered).toBe(expected);
- });
-
- it("converts images, codeblocks, blockquotes and nested blockquotes correctly", () => {
- const markdown = `##### Header 5
-
-An image example:
-
-
-###### Header 6
-
-Some **bold text** and *italic text* _italic text_.
-
-Code block example:
-\`\`\`
-javascript
-function greet() {
- console.log('Hello!');
-}
-\`\`\`
-
-Here is a block quote example:
-
-> This is a block quote.
-> It can span multiple lines.
-> > This is a nested quote
-> > With multiple
-> > Lines of code
-
-> Block quotes are often used to highlight important information or provide references.`;
-
- const expected = `Header 5
-
-An image example:
-
-
-Header 6
-
-Some bold text and italic text italic text .
-
-Code block example:
-
- javascript
- function greet() {
- console.log('Hello!');
- }
-
-
-Here is a block quote example:
-
-
-This is a block quote.
-It can span multiple lines.
-
-This is a nested quote
-With multiple
-Lines of code
-
-
-
-
-Block quotes are often used to highlight important information or provide references.
- `;
-
- const rendered = parseMarkdownToReactEmail(markdown);
- expect(rendered).toBe(expected);
- });
-});
diff --git a/__tests__/parseMarkdownToReactEmailJSX.test.ts b/__tests__/parseMarkdownToReactEmailJSX.test.ts
index 4f9a6c2..ae443e7 100644
--- a/__tests__/parseMarkdownToReactEmailJSX.test.ts
+++ b/__tests__/parseMarkdownToReactEmailJSX.test.ts
@@ -1,6 +1,88 @@
import { parseMarkdownToReactEmailJSX } from "../src";
describe("Markdown to React Mail JSX Parser", () => {
+ it("handles markdown correctly", () => {
+ const markdown = `# Markdown Test Document
+
+This is a **test document** to check the capabilities of a Markdown parser.
+
+## Headings
+
+### Third-Level Heading
+
+#### Fourth-Level Heading
+
+##### Fifth-Level Heading
+
+###### Sixth-Level Heading
+
+## Text Formatting
+
+This is some **bold text** and this is some *italic text*. You can also use ~~strikethrough~~ and \`inline code\`.
+
+## Lists
+
+1. Ordered List Item 1
+2. Ordered List Item 2
+3. Ordered List Item 3
+
+- Unordered List Item 1
+- Unordered List Item 2
+- Unordered List Item 3
+
+## Links
+
+[Markdown Guide](https://www.markdownguide.org)
+
+## Images
+
+
+
+## Blockquotes
+
+> This is a blockquote.
+> - Author
+
+## Code Blocks
+
+\`\`\`javascript
+function greet(name) {
+console.log(\`Hello, \$\{name\}!\`);
+}
+\`\`\``;
+
+ const expected = `
Markdown Test Document This is a test document to check the capabilities of a Markdown parser.
+Headings Third-Level Heading Fourth-Level Heading Fifth-Level Heading Sixth-Level Heading Text Formatting This is some bold text and this is some italic text . You can also use strikethrough and inline code.
+Lists
+Ordered List Item 1
+Ordered List Item 2
+Ordered List Item 3
+
+
+Unordered List Item 1
+Unordered List Item 2
+Unordered List Item 3
+
+Links Markdown Guide
+Images
+Blockquotes
+This is a blockquote.
+
+
+Code Blocks function greet(name) {
+console.log(\`Hello, $\{name\}!\`);
+}
+
+`;
+
+ const rendered = parseMarkdownToReactEmailJSX({
+ markdown,
+ });
+ expect(rendered).toBe(expected);
+ });
+
it("handles empty string correctly", () => {
const markdown = "";
const expected = ``;
@@ -19,13 +101,7 @@ describe("Markdown to React Mail JSX Parser", () => {
##### Header 5
###### Header 6
`;
- const expected = `Header 1
-Header 2
-Header 3
-Header 4
-Header 5
-Header 6
-`;
+ const expected = `Header 1 Header 2 Header 3 Header 4 Header 5 Header 6 `;
const rendered = parseMarkdownToReactEmailJSX({
markdown,
@@ -39,9 +115,7 @@ This is one
This is two
`;
- const expected = `The paragraphs
-This is one
-
+ const expected = `The paragraphs This is one
This is two
`;
@@ -53,13 +127,12 @@ This is two
it("handles bold, italic and strikethrough texts correctly", () => {
const markdown = `# The text formats
-This is **one** bold and _italic_ text
+This is **one** bold and *italic* text
This is ~~striked~~ text and \`inline code\``;
- const expected = `The text formats
-This is one bold and italic text
-
-This is striked text and inline code
`;
+ const expected = `The text formats This is one bold and italic text
+This is striked text and inline code
+`;
const rendered = parseMarkdownToReactEmailJSX({
markdown,
@@ -79,17 +152,19 @@ Here is an ordered list:
2. Second
3. Third
`;
- const expected = `The lists
-Here is an unordered list:
-
Here is an ordered list:
-First
+
+First
Second
Third
- `;
+
+`;
const rendered = parseMarkdownToReactEmailJSX({
markdown,
@@ -106,7 +181,23 @@ Here is an ordered list:
| Cell 3 | Cell 4 |
`;
const expected = `A table example:
-Column 1 Column 2 Cell 1 Cell 2 Cell 3 Cell 4
`;
+
+
+
+Column 1
+Column 2
+
+
+
+Cell 1
+Cell 2
+
+
+Cell 3
+Cell 4
+
+
+`;
const rendered = parseMarkdownToReactEmailJSX({
markdown,
@@ -125,11 +216,13 @@ greet("World")
\`\`\``;
const expected = `A example:
- python
- def greet(name):
- print(f"Hello, {name}!")
- greet("World")
- `;
+python
+def greet(name):
+print(f"Hello, {name}!")
+
+greet("World")
+
+`;
const rendered = parseMarkdownToReactEmailJSX({
markdown,
@@ -144,9 +237,10 @@ greet("World")
const expected = `A example:
-Here's a block quote:
-Lorem ipsum dolor sit amet, consectetur adipiscing elit.
- `;
+Here's a block quote:
+Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+
+`;
const rendered = parseMarkdownToReactEmailJSX({
markdown,
@@ -158,8 +252,9 @@ greet("World")
const markdown = `A example:
`;
- const expected = `A example:
- `;
+ const expected = `A example:
+
+`;
const rendered = parseMarkdownToReactEmailJSX({
markdown,
@@ -171,8 +266,9 @@ greet("World")
const markdown = `A link example:
Here's a link to [OpenAI's website](https://openai.com/).`;
- const expected = `A link example:
-Here's a link to OpenAI's website .
`;
+ const expected = `A link example:
+Here's a link to OpenAI's website .
+`;
const rendered = parseMarkdownToReactEmailJSX({
markdown,
diff --git a/__tests__/reactEmailMarkdown.test.tsx b/__tests__/reactEmailMarkdown.test.tsx
index 3f13eff..e701a5f 100644
--- a/__tests__/reactEmailMarkdown.test.tsx
+++ b/__tests__/reactEmailMarkdown.test.tsx
@@ -1,6 +1,6 @@
import React from "react";
import { render } from "@react-email/render";
-import { ReactEmailMarkdown } from "../src/components";
+import { ReactEmailMarkdown } from "../src";
describe("ReactEmailMarkdown component renders correctly", () => {
it("renders the markdown in the correct format for browsers", () => {
@@ -8,7 +8,7 @@ describe("ReactEmailMarkdown component renders correctly", () => {
);
expect(actualOutput).toMatchInlineSnapshot(
- `"
Hello, World! "`
+ `"
Hello, World! "`
);
});
});
diff --git a/package.json b/package.json
index 34e5bf1..fb19b21 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,6 @@
],
"scripts": {
"build": "tsup",
- "demo": "npx tsc src/demo && node src/demo",
"lint": "tsc",
"release": "pnpm run build && changeset publish",
"test": "jest --coverage",
@@ -34,6 +33,7 @@
},
"license": "MIT",
"devDependencies": {
+ "@changesets/cli": "^2.26.2",
"@react-email/render": "0.0.7",
"@types/jest": "29.5.2",
"@types/react": "18.2.8",
@@ -43,7 +43,6 @@
"ts-jest": "29.1.0",
"ts-node": "10.9.1",
"tsup": "6.7.0",
- "@changesets/cli": "^2.26.2",
"typescript": "5.1.3"
},
"peerDependencies": {
@@ -51,7 +50,8 @@
"react-email": ">1.9.3"
},
"dependencies": {
- "isomorphic-dompurify": "1.7.0"
+ "isomorphic-dompurify": "1.7.0",
+ "marked": "7.0.4"
},
"publishConfig": {
"access": "public"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5dbacd1..8d52b74 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4,6 +4,9 @@ dependencies:
isomorphic-dompurify:
specifier: 1.7.0
version: 1.7.0
+ marked:
+ specifier: 7.0.4
+ version: 7.0.4
devDependencies:
'@changesets/cli':
@@ -3992,6 +3995,12 @@ packages:
engines: {node: '>=8'}
dev: true
+ /marked@7.0.4:
+ resolution: {integrity: sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==}
+ engines: {node: '>= 16'}
+ hasBin: true
+ dev: false
+
/meow@6.1.1:
resolution: {integrity: sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==}
engines: {node: '>=8'}
diff --git a/pre-planning/BASE-STYLES.md b/pre-planning/BASE-STYLES.md
deleted file mode 100644
index 7a41987..0000000
--- a/pre-planning/BASE-STYLES.md
+++ /dev/null
@@ -1,68 +0,0 @@
-h1, h2, h3, h4, h5, h6 {
-font-weight: 500;
-padding-top: 20px;
-}
-
-h1 {
-font-size: 2.5rem;
-}
-
-h2 {
-font-size: 2rem;
-}
-
-h3 {
-font-size: 1.75rem;
-}
-
-.h4, h4 {
-font-size: 1.5rem;
-}
-
-.h5, h5 {
-font-size: 1.25rem;
-}
-
-.h6, h6 {
-font-size: 1rem;
-}
-
-bold{
-font-weight: bold;
-}
-
-italic {
-font-style: italic;
-}
-
-blockquote {
-background: #f9f9f9;
-border-left: 10px solid #ccc;
-margin: 1.5em 10px;
-padding: 1em 10px 0.1em 10px;
-quotes: "\201C""\201D""\2018""\2019";
-}
-
-code {
-color: #212529;
-font-size: 87.5%;
-word-wrap: break-word;
-display: inline;
-background: #f8f8f8;
-font-family: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
-}
-
-code-block {
-padding-top: 10px;
-padding-right: 10px;
-padding-left: 10px;
-padding-bottom: 1px;
-margin-bottom: 20px;
-background: #f8f8f8;
-}
-
-link {
-color: #007bff;
-text-decoration: underline;
-background-color: transparent;
-}
diff --git a/pre-planning/IDEAS.md b/pre-planning/IDEAS.md
deleted file mode 100644
index 687c57e..0000000
--- a/pre-planning/IDEAS.md
+++ /dev/null
@@ -1,5 +0,0 @@
-- export package for use on web ui
-- export directly as reactmail component ✅
-- pass custom components for different md tags
-- pass custom styles for different md tags ✅
-- custom matchers for md tags for different flavors
diff --git a/pre-planning/MATCHER-CASES.md b/pre-planning/MATCHER-CASES.md
deleted file mode 100644
index 537cc31..0000000
--- a/pre-planning/MATCHER-CASES.md
+++ /dev/null
@@ -1,50 +0,0 @@
-// Handle headings (e.g., # Heading)
-(/^#\s+(.+)$/gm, "$1 ");
- (/^##\s+(.+)$/gm, "$1 ");
- (/^###\s+(.+)$/gm, "$1 ");
- (/^####\s+(.+)$/gm, "$1 ");
- (/^#####\s+(.+)$/gm, "$1 ");
- (/^######\s+(.+)$/gm, "$1 ");
-
-// Handle paragraphs
-(
-/((\n|^)(?!\n)((?!<\/?(h|ul|ol|p|pre|div|blockquote)[>\s]).)+(\n|$)+)+/gm,
- "$&
"
-);
-
-// Handle bold text (e.g., **bold**)
-(/\*\*(.+?)\*\*/g, "$1 ");
-
-// Handle italic text (e.g., _italic_)
-(/\*(.+?)\*/g, "$1 ");
-
-// Handle unordered lists
-(/^\s*-\s+(.*)$/gm, "$1 ");
- (/<\/li>\n/g, " ");
-``;
-
-// Handle ordered lists
-(/^\s*\d+\.\s+(.*)$/gm, " $1 ");
- (/<\/li>\n/g, " ");
-`${markdown} `;
-
-// Handle links (e.g., [link text](url))
-(/\[(.+?)\]\((.\*?)\)/g, '$1 ');
-
-// Handle images (e.g., )
-(
-/!\[(._?)\]\((._?)\)/g,
-' '
-);
-
-// Handle code blocks (e.g., `code`)
-(/`(.+?)`/gs, "$1 ");
-
-// Handle inline code (e.g., `code`)
-(/`(.+?)`/g, "$1");
-
-// Handle line breaks (e.g., )
-(/ \n/g, " ");
-
-// Handle horizontal rules (e.g., ---)
-(/^-{3,}$/gm, " ");
diff --git a/pre-planning/PROJECT-BREAKDOWN.md b/pre-planning/PROJECT-BREAKDOWN.md
deleted file mode 100644
index 0da0b1a..0000000
--- a/pre-planning/PROJECT-BREAKDOWN.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# Project breakdown
-
-## Functions:
-
-- `camelToKebabCase`: converts strings from camelcase ['thisIsCamelCase'] to kebab case ['this-is-kebab-case']
-- `parseCssInJsToInlineCss`: converts css styles from css-in-js to inline css e.g fontSize: "18px" => font-size: 18px;
-- `parseMarkdownToReactEmail`: parses markdown to a valid react-email string that can be copied and pasted directly into your codebase
-- `parseMarkdownToReactEmailJSX`: parses markdown to valid react-email JSX for the client (i.e the browser)
-
-## Components:
-
-- `ReactEmailMarkdown`: a react-email component that takes in markdown input and parses it directly in your code base
-
-## Usage:
-
-- Directly as `React-email` component
-
-```
-import {ReactEmailMarkdown} from "md-to-react-email"
-
-export default function EmailTemplate() {
- return (
-
-
-
-
- )
- }
-```
-
-- Directly into react-email template
-
-```
-import {parseMarkdownToReactEmailJSX} from "md-to-react-email"
-
-const markdown = `# Hello World`
-const parsedReactMail = parseMarkdownToReactEmail(markdown)
-
-console.log(parsedReactMail) // ` `
-
-```
-
-- For code generation (copy and paste)
-
-```
-import {parseMarkdownToReactEmail} from "md-to-react-email"
-
-const markdown = `# Hello World`
-const parsedReactMail = parseMarkdownToReactEmail(markdown)
-
-console.log(parsedReactMail) // ` `
-
-```
diff --git a/pre-planning/PROJECT-OUTLINE.md b/pre-planning/PROJECT-OUTLINE.md
deleted file mode 100644
index 76964ac..0000000
--- a/pre-planning/PROJECT-OUTLINE.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# Project breakdown
-
-### Note: This is a non opinionated project.
-
-- Create some basic styles for all our components
-
- - Headers h1-h6
- - Paragraphs
- - Quotes
- - Lists - ordered and unordered
- - Code Blocks
- - Images
- - Horizontal Rules
- - Line breaks
- - Links
-
-- ReactMail (rm)
-- Input markdown (md)
-- Function mdToRm
-- Output valid RM e.g (# A boy => A boy )
-
-# General file structure
-
-- Styles file(css-in-js): contains all the basic styles
-
- > CSS
- > .header{
- > font-size: 16px
- > } >
- > CSS-in-JS
- > const header = {
- > fontSize: 16
- > }
-
-- Index.js: one function that takes in md and spits out rm with styles already in place
diff --git a/pre-planning/REPLACEMENT-CASES.md b/pre-planning/REPLACEMENT-CASES.md
deleted file mode 100644
index bd8082d..0000000
--- a/pre-planning/REPLACEMENT-CASES.md
+++ /dev/null
@@ -1,43 +0,0 @@
-```
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-- Headers h1-h6
-
-- Paragraphs
-
-- Quotes
-
-
-- Lists - ordered and unordered
-- Inline code
-
-- Code Blocks
-
-
-- Images
-
-- Horizontal Rules
-
-- Line breaks
-
-- Links
-
diff --git a/pre-planning/TODOS.md b/pre-planning/TODOS.md
deleted file mode 100644
index 3795581..0000000
--- a/pre-planning/TODOS.md
+++ /dev/null
@@ -1,3 +0,0 @@
-- Extensive testing of packages and components
-- Create NPM package
-- Export component directly into RM teplate✅
diff --git a/src/.DS_Store b/src/.DS_Store
index d17bb8d..18d0551 100644
Binary files a/src/.DS_Store and b/src/.DS_Store differ
diff --git a/src/components/reactEmailMarkdown.tsx b/src/components/reactEmailMarkdown.tsx
index 21417e6..8fc5634 100644
--- a/src/components/reactEmailMarkdown.tsx
+++ b/src/components/reactEmailMarkdown.tsx
@@ -1,6 +1,6 @@
import * as React from "react";
import { StylesType } from "../types";
-import { parseMarkdownToReactEmailJSX } from "../utils";
+import { parseMarkdownToReactEmailJSX } from "../parseMarkdownToReactEmailJSX";
interface ReactEmailMarkdownProps {
markdown: string;
diff --git a/src/hooks.ts b/src/hooks.ts
new file mode 100644
index 0000000..155388d
--- /dev/null
+++ b/src/hooks.ts
@@ -0,0 +1,17 @@
+import DOMPurify from "isomorphic-dompurify";
+
+DOMPurify.addHook("afterSanitizeAttributes", (node) => {
+ if ("target" in node) {
+ node.setAttribute("target", "_blank");
+ }
+});
+
+function preprocess(markdown: string) {
+ return markdown;
+}
+
+function postprocess(html: string): string {
+ return DOMPurify.sanitize(html);
+}
+
+export const hooks = { preprocess, postprocess };
diff --git a/src/index.ts b/src/index.ts
index 55d88fc..cec4b0f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,29 +1,19 @@
-/**
- * Default Styles
- */
-export { styles } from "./styles";
-
-/**
- * Default Matchers
- */
-export { patterns } from "./patterns";
-
/**
* Types
*/
-export { Patterns, StylesType } from "./types";
+export { StylesType, ParseMarkdownToReactEmailJSXProps } from "./types";
/**
- * Functions
+ * Utility Functions
*/
-export {
- camelToKebabCase,
- parseCssInJsToInlineCss,
- parseMarkdownToReactEmail,
- parseMarkdownToReactEmailJSX,
-} from "./utils";
+export { parseMarkdownToReactEmailJSX } from "./parseMarkdownToReactEmailJSX";
/**
* Components
*/
export { ReactEmailMarkdown } from "./components";
+
+/**
+ * String Utils
+ */
+export { camelToKebabCase, parseCssInJsToInlineCss } from "./utils";
diff --git a/src/parseMarkdownToReactEmailJSX.ts b/src/parseMarkdownToReactEmailJSX.ts
new file mode 100644
index 0000000..266be00
--- /dev/null
+++ b/src/parseMarkdownToReactEmailJSX.ts
@@ -0,0 +1,11 @@
+import { MarkdownParser } from "./parser";
+import { ParseMarkdownToReactEmailJSXProps } from "./types";
+
+export const parseMarkdownToReactEmailJSX = ({
+ markdown,
+ customStyles,
+ withDataAttr,
+}: ParseMarkdownToReactEmailJSXProps) => {
+ const parser = new MarkdownParser({ customStyles, withDataAttr });
+ return parser.parse(markdown);
+};
diff --git a/src/parser.ts b/src/parser.ts
new file mode 100644
index 0000000..e91c420
--- /dev/null
+++ b/src/parser.ts
@@ -0,0 +1,27 @@
+import { marked, RendererObject } from "marked";
+import { StylesType } from "./types";
+import { initRenderer } from "./utils";
+import { hooks } from "./hooks";
+
+export class MarkdownParser {
+ private readonly renderer: RendererObject;
+
+ constructor({
+ customStyles,
+ withDataAttr,
+ }: {
+ customStyles?: StylesType;
+ withDataAttr?: boolean;
+ }) {
+ this.renderer = initRenderer({ customStyles, withDataAttr });
+ }
+
+ parse(markdown: string) {
+ marked.use({
+ renderer: this.renderer,
+ hooks,
+ });
+
+ return marked.parse(markdown);
+ }
+}
diff --git a/src/patterns.ts b/src/patterns.ts
deleted file mode 100644
index 77a4bca..0000000
--- a/src/patterns.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-export const patterns = {
- h1: /^#\s+(.+)$/gm,
- h2: /^##\s+(.+)$/gm,
- h3: /^###\s+(.+)$/gm,
- h4: /^####\s+(.+)$/gm,
- h5: /^#####\s+(.+)$/gm,
- h6: /^######\s+(.+)$/gm,
- p: /^(?!#{1,6}\s)((?!<(pre|blockquote|Text)\b[^>]*>)(?!.*<\/(pre|blockquote|Text)>$)((?!(?:[-*+\s]+|\d+\.\s+|#\s+|.*?\|.*?\||!\[.*?\]\(.*?\)|```\s*\n(?:.|\n)+?```| {4}(?:.|\n)+?))(?:.|\n)+?))(?=\n{2,}|$)/gm,
- bold: /\*\*(.+?)\*\*/g,
- italic: /([*_])(.*?)\1/g,
- li: /^\s*[-|\*]\s+(.*)$/gm,
- ul: /()(?![\s\S]*<\/ul>)/gs,
- ol: /^(?:\d+\.\s+.+$(?:\n(?!$).+)*(?:\n|$))+/gm,
- image: /!\[(.*?)\]\((.*?)\)/g,
- link: /\[(.+?)\]\((.*?)\)/g,
- blockQuote: /^>(?: .+?(?:\n|$))+/gm,
- nestedBlockQuote: /^>( .+?(?:\n|$))+/gm,
- codeBlocks: /```(?:[\s\S]*?\n)?([\s\S]*?)\n```/g,
- codeInline: /(? {
- if ("target" in node) {
- node.setAttribute("target", "_blank");
- }
-});
+import { StylesType, initRendererProps } from "./types";
+import { RendererObject } from "marked";
+import { styles } from "./styles";
export function camelToKebabCase(str: string): string {
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
@@ -20,625 +12,246 @@ export function parseCssInJsToInlineCss(
): string {
if (!cssProperties) return "";
+ const numericalCssProperties = [
+ "width",
+ "height",
+ "margin",
+ "marginTop",
+ "marginRight",
+ "marginBottom",
+ "marginLeft",
+ "padding",
+ "paddingTop",
+ "paddingRight",
+ "paddingBottom",
+ "paddingLeft",
+ "borderWidth",
+ "borderTopWidth",
+ "borderRightWidth",
+ "borderBottomWidth",
+ "borderLeftWidth",
+ "outlineWidth",
+ "top",
+ "right",
+ "bottom",
+ "left",
+ "fontSize",
+ "lineHeight",
+ "letterSpacing",
+ "wordSpacing",
+ "maxWidth",
+ "minWidth",
+ "maxHeight",
+ "minHeight",
+ "borderRadius",
+ "borderTopLeftRadius",
+ "borderTopRightRadius",
+ "borderBottomLeftRadius",
+ "borderBottomRightRadius",
+ "textIndent",
+ "gridColumnGap",
+ "gridRowGap",
+ "gridGap",
+ "translateX",
+ "translateY",
+ ];
+
return Object.entries(cssProperties)
- .map(([property, value]) => `${camelToKebabCase(property)}:${value}`)
+ .map(([property, value]) => {
+ if (
+ typeof value === "number" &&
+ numericalCssProperties.includes(property)
+ ) {
+ return `${camelToKebabCase(property)}:${value}px`;
+ } else {
+ return `${camelToKebabCase(property)}:${value}`;
+ }
+ })
.join(";");
}
-export function parseMarkdownToReactEmail(
- markdown: string,
- customStyles?: StylesType
-): string {
+export const initRenderer = ({
+ customStyles,
+ withDataAttr = false,
+}: initRendererProps): RendererObject => {
const finalStyles = { ...styles, ...customStyles };
- let reactMailTemplate = "";
- // Handle inline code (e.g., `code`)
- reactMailTemplate = markdown.replace(
- patterns.codeInline,
- `$2 `
- );
-
- // Handle code blocks (e.g., ```code```)
- reactMailTemplate = reactMailTemplate.replace(
- patterns.codeBlocks,
- function (_, codeContent: string) {
- const indentedCodeContent = codeContent
- .split("\n")
- .map((line) => ` ${line}`)
- .join("\n");
- return `\n${indentedCodeContent}\n `;
- }
- );
+ }>\n${quote}\n`;
+ },
- // Handle blockquotes
- function parseMarkdownWithBlockQuotes(markdown: string): string {
- const blockquoteRegex = /^(>\s*((?:.+\n?)+))(?!\n(?=>\s))/gm;
-
- function parseBlockQuote(match: string) {
- const nestedContent = match.replace(/^>\s*/gm, "");
- const nestedHTML = parseMarkdownWithBlockQuotes(nestedContent);
- return `\n${nestedHTML}\n `;
- }
-
- return markdown.replace(blockquoteRegex, parseBlockQuote);
- }
-
- reactMailTemplate = parseMarkdownWithBlockQuotes(reactMailTemplate);
-
- // Handle paragraphs
- reactMailTemplate = reactMailTemplate.replace(
- patterns.p,
- `$1 `
- );
-
- // Handle headings (e.g., # Heading)
- reactMailTemplate = reactMailTemplate.replace(
- patterns.h1,
- `$1 `
- );
- reactMailTemplate = reactMailTemplate.replace(
- patterns.h2,
- `$1 `
- );
- reactMailTemplate = reactMailTemplate.replace(
- patterns.h3,
- `$1 `
- );
- reactMailTemplate = reactMailTemplate.replace(
- patterns.h4,
- `$1 `
- );
- reactMailTemplate = reactMailTemplate.replace(
- patterns.h5,
- `$1 `
- );
- reactMailTemplate = reactMailTemplate.replace(
- patterns.h6,
- `$1 `
- );
-
- // Handle Tables from GFM
- reactMailTemplate = reactMailTemplate.replace(
- patterns.table,
- (match: string) => {
- const rows = match.trim().split("\n");
- const headers = rows[0]
- .split("|")
- .slice(1, -1)
- .map((cell) => cell.trim());
- const alignments = rows[1]
- .split("|")
- .slice(1, -1)
- .map((cell) => {
- const align = cell.trim().toLowerCase();
- return align === ":--"
- ? "left"
- : align === "--:"
- ? "right"
- : "center";
- });
- const body = rows
- .slice(2)
- .map((row) => {
- const cells = row
- .split("|")
- .slice(1, -1)
- .map((cell) => cell.trim());
- return `${cells
- .map(
- (cell, index) =>
- `${cell} `
- )
- .join("")} `;
- })
- .join("");
-
- const table = `${headers
- .map(
- (header, index) =>
- `${header} `
- )
- .join("")} ${body}
`;
- return table;
- }
- );
-
- // Handle strikethrough
- reactMailTemplate = reactMailTemplate.replace(
- patterns.strikethrough,
- `$1`
- );
-
- // Handle bold text (e.g., **bold**)
- reactMailTemplate = reactMailTemplate.replace(
- patterns.bold,
- `$1 `
- );
-
- // Handle italic text (e.g., *italic*)
- reactMailTemplate = reactMailTemplate.replace(
- patterns.italic,
- `$2 `
- );
-
- // Handle lists (unordered)
- reactMailTemplate = reactMailTemplate.replace(
- patterns.li,
- `$1 `
- );
- reactMailTemplate = reactMailTemplate.replace(
- patterns.ul,
- ``
- );
-
- // Handle lists (ordered)
- reactMailTemplate = reactMailTemplate.replace(patterns.ol, function (match) {
- const listItems = match
- .split("\n")
- .map((line) => {
- const listItemContent = line.replace(/^\d+\.\s+/, "");
- return listItemContent
- ? `${listItemContent} `
- : "";
- })
- .join("\n");
- return `${listItems} `;
- });
-
- // Handle images (e.g., )
- reactMailTemplate = reactMailTemplate.replace(
- patterns.image,
- ` `
- );
-
- // Handle links (e.g., [link text](url))
- reactMailTemplate = reactMailTemplate.replace(
- patterns.link,
- ` $1`
- );
-
- // Handle line breaks (e.g., )
- reactMailTemplate = reactMailTemplate.replace(
- patterns.br,
- ` `
- );
-
- // Handle horizontal rules (e.g., ---)
- reactMailTemplate = reactMailTemplate.replace(
- patterns.hr,
- ` `
- );
-
- reactMailTemplate = ``;
-
- return reactMailTemplate;
-}
-
-interface ParseMarkdownToReactEmailJSXProps {
- markdown: string;
- customStyles?: StylesType;
- withDataAttr?: boolean;
-}
+ } />`;
+ },
-export function parseMarkdownToReactEmailJSX({
- markdown,
- customStyles,
- withDataAttr = false,
-}: ParseMarkdownToReactEmailJSXProps): string {
- if (
- markdown === undefined ||
- markdown === null ||
- markdown === "" ||
- typeof markdown !== "string"
- ) {
- return "";
- }
+ code(code) {
+ code = code.replace(/\n$/, "") + "\n";
- const finalStyles = { ...styles, ...customStyles };
- let reactMailTemplate = "";
-
- // Handle inline code (e.g., `code`)
- reactMailTemplate = markdown.replace(
- patterns.codeInline,
- `$2`
- );
-
- // Handle code blocks (e.g., ```code```)
- reactMailTemplate = reactMailTemplate.replace(
- patterns.codeBlocks,
- function (_, codeContent: string) {
- const indentedCodeContent = codeContent
- .split("\n")
- .map((line) => ` ${line}`)
- .join("\n");
return `\n${indentedCodeContent}\n `;
- }
- );
+ }>${code}\n`;
+ },
- // Handle blockquotes
- function parseMarkdownWithBlockQuotes(markdown: string): string {
- const blockquoteRegex = /^(>\s*((?:.+\n?)+))(?!\n(?=>\s))/gm;
+ codespan(text) {
+ return `${text}`;
+ },
- function parseBlockQuote(match: string) {
- const nestedContent = match.replace(/^>\s*/gm, "");
- const nestedHTML = parseMarkdownWithBlockQuotes(nestedContent);
- return `\n${nestedHTML}\n `;
- }
+ }>${text}`;
+ },
- return markdown.replace(blockquoteRegex, parseBlockQuote);
- }
+ em(text) {
+ return `${text} `;
+ },
+
+ heading(text, level) {
+ return `${text} `;
+ },
+
+ hr() {
+ return ` \n`;
+ },
- reactMailTemplate = parseMarkdownWithBlockQuotes(reactMailTemplate);
+ image(href, _, text) {
+ let out = ` `;
+ return out;
+ },
+
+ link(href, _, text) {
+ let out = `${text} `;
+ return out;
+ },
+
+ list(body, ordered, start) {
+ const type = ordered ? "ol" : "ul";
+ const startatt = ordered && start !== 1 ? ' start="' + start + '"' : "";
+ return (
+ "<" +
+ type +
+ startatt +
+ `${
+ parseCssInJsToInlineCss(finalStyles.ol) !== ""
+ ? ` style="${parseCssInJsToInlineCss(
+ finalStyles[ordered ? "ol" : "ul"]
+ )}"`
+ : ""
+ } >\n` +
+ body +
+ "" +
+ type +
+ ">\n"
+ );
+ },
+
+ listitem(text) {
+ return `${text} \n`;
+ },
- // Handle paragraphs
- reactMailTemplate = reactMailTemplate.replace(
- patterns.p,
- `$1
`
- );
+ paragraph(text) {
+ return `${text}
\n`;
+ },
- // Handle headings (e.g., # Heading)
- reactMailTemplate = reactMailTemplate.replace(
- patterns.h1,
- `$1 `
- );
- reactMailTemplate = reactMailTemplate.replace(
- patterns.h2,
- `$1 `
- );
- reactMailTemplate = reactMailTemplate.replace(
- patterns.h3,
- `$1 `
- );
- reactMailTemplate = reactMailTemplate.replace(
- patterns.h4,
- `$1 `
- );
- reactMailTemplate = reactMailTemplate.replace(
- patterns.h5,
- `$1 `
- );
- reactMailTemplate = reactMailTemplate.replace(
- patterns.h6,
- `$1 `
- );
+ strong(text) {
+ return `${text} `;
+ },
- // Handle Tables from GFM
- reactMailTemplate = reactMailTemplate.replace(
- patterns.table,
- (match: string) => {
- const rows = match.trim().split("\n");
- const headers = rows[0]
- .split("|")
- .slice(1, -1)
- .map((cell) => cell.trim());
- const alignments = rows[1]
- .split("|")
- .slice(1, -1)
- .map((cell) => {
- const align = cell.trim().toLowerCase();
- return align === ":--"
- ? "left"
- : align === "--:"
- ? "right"
- : "center";
- });
- const body = rows
- .slice(2)
- .map((row) => {
- const cells = row
- .split("|")
- .slice(1, -1)
- .map((cell) => cell.trim());
- return `${cells
- .map(
- (cell, index) =>
- `${cell} `
- )
- .join("")} `;
- })
- .join("");
+ table(header, body) {
+ if (body) body = `${body} `;
- const table = `\n`;
+ },
+
+ tablecell(content, flags) {
+ const type = flags.header ? "th" : "td";
+ const tag = flags.align
+ ? `<${type} align="${flags.align}"${
+ parseCssInJsToInlineCss(finalStyles.td) !== ""
+ ? ` style="${parseCssInJsToInlineCss(finalStyles.td)}"`
+ : ""
+ }>`
+ : `<${type}${
+ parseCssInJsToInlineCss(finalStyles.td) !== ""
+ ? ` style="${parseCssInJsToInlineCss(finalStyles.td)}"`
+ : ""
+ }>`;
+ return tag + content + `${type}>\n`;
+ },
+
+ tablerow(content) {
+ return `${headers
- .map(
- (header, index) =>
- `${header} `
- )
- .join("")} ${body} `;
- return table;
- }
- );
-
- // Handle strikethrough
- reactMailTemplate = reactMailTemplate.replace(
- patterns.strikethrough,
- `$1`
- );
-
- // Handle bold text (e.g., **bold**)
- reactMailTemplate = reactMailTemplate.replace(
- patterns.bold,
- `$1 `
- );
+ }>\n${content}\n`;
+ },
+ };
- // Handle italic text (e.g., *italic*)
- reactMailTemplate = reactMailTemplate.replace(
- patterns.italic,
- `$2 `
- );
-
- // Handle lists (unordered)
- reactMailTemplate = reactMailTemplate.replace(
- patterns.li,
- `$1 `
- );
- reactMailTemplate = reactMailTemplate.replace(
- patterns.ul,
- ``
- );
-
- // Handle lists (ordered)
- reactMailTemplate = reactMailTemplate.replace(patterns.ol, function (match) {
- const listItems = match
- .split("\n")
- .map((line) => {
- const listItemContent = line.replace(/^\d+\.\s+/, "");
- return listItemContent
- ? `${listItemContent} `
- : "";
- })
- .join("\n");
- return `${listItems} `;
- });
-
- // Handle images (e.g., )
- reactMailTemplate = reactMailTemplate.replace(
- patterns.image,
- ` `
- );
-
- // Handle links (e.g., [link text](url))
- reactMailTemplate = reactMailTemplate.replace(
- patterns.link,
- `$1 `
- );
-
- // Handle line breaks (e.g., )
- reactMailTemplate = reactMailTemplate.replace(
- patterns.br,
- ` `
- );
-
- // Handle horizontal rules (e.g., ---)
- reactMailTemplate = reactMailTemplate.replace(
- patterns.hr,
- ` `
- );
-
- return DOMPurify.sanitize(reactMailTemplate, {
- USE_PROFILES: { html: true },
- });
-}
+ return customRenderer;
+};