Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 24 additions & 30 deletions packages/jsx-email/src/components/conditional.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,32 @@
import React, { Suspense } from 'react';
import React from 'react';

import { jsxToString } from '../renderer/jsx-to-string.js';
import { useData } from '../renderer/suspense.js';
import type { JsxEmailComponent } from '../types.js';

declare module 'react/jsx-runtime' {
namespace JSX {
interface IntrinsicElements {
// @ts-ignore
'jsx-email-cond': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement> & {
'data-expression'?: string;
'data-head'?: boolean;
'data-mso'?: boolean;
},
HTMLElement
>;
}
Comment on lines +5 to +17
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom JSX intrinsic element typing relies on // @ts-ignore inside the module augmentation. That effectively disables type checking right at the point you’re trying to make it safer, and it’s easy for that to spread.

Given this is a library component, it’s worth making the augmentation clean so consumers (and this repo) don’t normalize @ts-ignore as acceptable.

Suggestion

Remove the // @ts-ignore and make the intrinsic element definition stand on its own.

In most setups, this works without suppression:

declare module 'react/jsx-runtime' {
  namespace JSX {
    interface IntrinsicElements {
      'jsx-email-cond': React.DetailedHTMLProps<
        React.HTMLAttributes<HTMLElement> & {
          'data-expression'?: string;
          'data-head'?: boolean;
          'data-mso'?: boolean;
        },
        HTMLElement
      >;
    }
  }
}

If TS still complains due to module resolution differences, prefer moving this augmentation into a dedicated *.d.ts (e.g. src/jsx.d.ts) that’s included by tsconfig, instead of suppressing errors inline.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

}
}

export interface ConditionalProps {
children?: React.ReactNode;
expression?: string;
head?: boolean;
mso?: boolean;
}

const notMso = (html: string) => `<!--[if !mso]><!-->${html}<!--<![endif]-->`;

const comment = (expression: string, html: string) => `<!--[if ${expression}]>${html}<![endif]-->`;

const Renderer = (props: ConditionalProps) => {
const { children, mso, head } = props;
let { expression } = props;
const html = useData(props, () => jsxToString(<>{children}</>));
let innerHtml = '';

if (mso === false) innerHtml = notMso(html);
else if (mso === true && !expression) expression = 'mso';
if (expression) innerHtml = comment(expression, html);

const Component = head ? 'head' : 'jsx-email-cond';

// @ts-ignore
// Note: This is perfectly valid. TS just expects lowercase tag names to match a specific type
return <Component dangerouslySetInnerHTML={{ __html: innerHtml }} />;
};

export const Conditional: JsxEmailComponent<ConditionalProps> = (props) => {
const { children, expression, mso } = props;
const { children, expression, mso, head } = props;

if (typeof expression === 'undefined' && typeof mso === 'undefined')
throw new RangeError(
Expand All @@ -45,12 +38,13 @@ export const Conditional: JsxEmailComponent<ConditionalProps> = (props) => {
'jsx-email: Conditional expects the `expression` or `mso` prop to be defined, not both'
);

// Always render a JSX custom element with data-* markers.
// A rehype plugin will replace this element with proper conditional comments.
// @ts-ignore - lower-case custom element tag is valid
return (
<>
<Suspense fallback={<div>waiting</div>}>
<Renderer {...props}>{children}</Renderer>
</Suspense>
</>
<jsx-email-cond data-mso={mso} data-expression={expression} data-head={head}>
{children}
</jsx-email-cond>
);
};

Expand Down
15 changes: 5 additions & 10 deletions packages/jsx-email/src/components/head.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { BaseProps, JsxEmailComponent } from '../types.js';
import { debug } from '../debug.js';
import type { BaseProps, JsxEmailComponent } from '../types.js';

import { Conditional } from './conditional.js';
import { Raw } from './raw.js';

export interface HeadProps extends BaseProps<'head'> {
enableFormatDetection?: boolean;
Expand All @@ -27,15 +28,9 @@ export const Head: JsxEmailComponent<HeadProps> = ({
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no, url=no" />
)}
{children}
<Conditional
head
mso
children={
// prettier-ignore
// @ts-expect-error: element don't exist
<xml><o:OfficeDocumentSettings><o:AllowPNG /><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml>
}
/>
<Conditional head mso>
<Raw content="<xml><o:OfficeDocumentSettings><o:AllowPNG /><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml>" />
</Conditional>
</head>
);

Expand Down
106 changes: 106 additions & 0 deletions packages/jsx-email/src/renderer/conditional.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import type { Content, Element, Literal, Parents, Root } from 'hast';

// dynamic import of 'unist-util-visit' within factory to support CJS build

interface Match {
index: number;
node: Element;
parent: Parents;
}

// `raw` is an unofficial HAST node used by rehype to pass through HTML verbatim.
// Model it locally to avoid `any` casts while keeping the rest of the tree typed.
interface Raw extends Literal {
type: 'raw';
value: string;
}

interface ParentWithRaw {
children: (Content | Raw)[];
}

/**
* Returns a rehype plugin that replaces `<jsx-email-cond>` elements (from
* the Conditional component) with conditional comment wrappers, based on the
* `data-mso` and `data-expression` attributes.
*
* Mirrors the async factory pattern used by `getRawPlugin()`.
*/
export const getConditionalPlugin = async () => {
const { visit } = await import('unist-util-visit');

return function conditionalPlugin() {
return function transform(tree: Root) {
const matches: Match[] = [];
let headEl: Element | undefined;

visit(tree, 'element', (node, index, parent) => {
if (node.tagName === 'head') headEl = node;

if (!parent || typeof index !== 'number') return;
if (node.tagName !== 'jsx-email-cond') return;

matches.push({ index, node, parent });
});

for (const { node, parent, index } of matches) {
const props = (node.properties || {}) as Record<string, unknown>;
const msoProp = (props['data-mso'] ?? (props as any).dataMso) as unknown;
const msoAttr =
typeof msoProp === 'undefined' ? void 0 : msoProp === 'false' ? false : Boolean(msoProp);
const exprRaw = (props['data-expression'] ?? (props as any).dataExpression) as unknown;
const exprAttr = typeof exprRaw === 'string' ? exprRaw : void 0;
const headProp = (props['data-head'] ?? (props as any).dataHead) as unknown;
const toHead =
typeof headProp === 'undefined'
? false
: headProp === 'false'
? false
: Boolean(headProp);
Comment on lines +47 to +59
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are multiple any casts here ((props as any).dataMso, etc.). This undermines the otherwise nice effort to keep HAST nodes typed and makes the attribute decoding harder to reason about.

Since properties is already Record<string, unknown>-like, you can avoid any by accessing camelCase keys through the same record and centralizing the coercion logic.

Suggestion

Introduce small helpers to normalize properties access without any, and share boolean parsing between data-mso and data-head.

const getProp = (p: Record<string, unknown>, dash: string, camel: string) =>
  (p[dash] ?? p[camel]) as unknown;

const parseBoolAttr = (v: unknown): boolean | undefined => {
  if (typeof v === 'undefined') return undefined;
  if (v === 'false') return false;
  return Boolean(v);
};

const msoAttr = parseBoolAttr(getProp(props, 'data-mso', 'dataMso'));
const toHead = parseBoolAttr(getProp(props, 'data-head', 'dataHead')) ?? false;
const exprRaw = getProp(props, 'data-expression', 'dataExpression');

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.


let openRaw: string | undefined;
let closeRaw: string | undefined;

if (msoAttr === false) {
// Not MSO: <!--[if !mso]><!--> ... <!--<![endif]-->
openRaw = '<!--[if !mso]><!-->';
closeRaw = '<!--<![endif]-->';
} else {
// MSO / expression path
const expression = exprAttr || (msoAttr === true ? 'mso' : void 0);
if (expression) {
openRaw = `<!--[if ${expression}]>`;
// Older Outlook/Word HTML parsers prefer the self-closing
// conditional terminator variant to avoid comment spillover
// when adjacent comments appear. Use the `<![endif]/-->` form
// for maximum compatibility.
closeRaw = '<![endif]/-->';
}
}

// If no directive attributes present, leave the element in place.
// eslint-disable-next-line no-continue
if (!openRaw || !closeRaw) continue;

const before: Raw = { type: 'raw', value: openRaw };
const after: Raw = { type: 'raw', value: closeRaw };
const children = (node.children || []) as Content[];

if (toHead && headEl) {
if (parent === headEl) {
// Replace in place: open raw, original children, close raw.
(parent as ParentWithRaw).children.splice(index, 1, before, ...children, after);
} else {
// Remove wrapper from current location
(parent as ParentWithRaw).children.splice(index, 1);
// Append the conditional to the <head>
(headEl as unknown as ParentWithRaw).children.push(before, ...children, after);
}
} else {
// Replace in place: open raw, original children, close raw.
(parent as ParentWithRaw).children.splice(index, 1, before, ...children, after);
}
}
Comment on lines +34 to +103
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getConditionalPlugin() collects (index, node, parent) matches, then mutates parents using splice(index, …) in a forward loop. This is fragile when multiple <jsx-email-cond> nodes share the same parent: removing/replacing an earlier sibling shifts indices of later matches, so later splice() calls can target the wrong node (or skip/duplicate content). The risk increases because you also sometimes remove the node from its original parent and append into <head>.

This is a correctness issue: an email with multiple conditionals in the same container could render incorrectly depending on traversal order.

Suggestion

Fix by applying mutations in reverse document order per parent (or by using stable references rather than stored indices). One simple approach:

  • Collect matches.
  • Sort by parent identity and index descending, then splice.

Example:

// after collecting matches
matches.sort((a, b) => {
  if (a.parent === b.parent) return b.index - a.index;
  return 0; // keep relative order across different parents
});

for (const m of matches) {
  // ... existing logic
}

Or, even safer: when moving to head, avoid relying on the stored index by removing via identity search:

const idx = (parent as ParentWithRaw).children.indexOf(node as any);
if (idx !== -1) (parent as ParentWithRaw).children.splice(idx, 1);

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

};
};
};
57 changes: 57 additions & 0 deletions packages/jsx-email/src/renderer/raw.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
import type { Comment, Content, Element, Literal, Parents, Root } from 'hast';

interface Match {
index: number;
node: Element;
parent: Parents;
}

interface ParentWithRaw {
children: (Content | Raw)[];
}

// `raw` is an unofficial HAST node used by rehype to pass through HTML verbatim.
// Model it locally to avoid `any` casts while keeping the rest of the tree typed.
interface Raw extends Literal {
type: 'raw';
value: string;
}

const START_TAG = '__COMMENT_START';
const END_TAG = '__COMMENT_END';
export function escapeForRawComponent(input: string): string {
Expand All @@ -10,3 +29,41 @@ export function unescapeForRawComponent(input: string): string {
.replace(new RegExp(START_TAG, 'g'), '<!--')
.replace(new RegExp(END_TAG, 'g'), '/-->');
}

/**
* Returns a rehype plugin that replaces `<jsx-email-raw><!--...--></jsx-email-raw>`
* elements with a raw HTML node using the original, unescaped content.
*
* Mirrors the async factory pattern used by `getMovePlugin()`.
*/
export const getRawPlugin = async () => {
const { visit } = await import('unist-util-visit');

return function rawPlugin() {
return function transform(tree: Root) {
const matches: Match[] = [];

visit(tree, 'element', (node, index, parent) => {
if (!parent || typeof index !== 'number') return;
if (node.tagName !== 'jsx-email-raw') return;

matches.push({ index, node: node as Element, parent });
});

for (const { node, parent, index } of matches) {
// The Raw component renders a single HTML comment child containing the
// escaped raw content. Extract it and unescape back to the original.
const commentChild = node.children.find((c): c is Comment => c.type === 'comment');

if (commentChild) {
const rawHtml = unescapeForRawComponent(commentChild.value);

// Replace the wrapper element with a `raw` node to inject HTML verbatim.
// rehype-stringify will pass this through when `allowDangerousHtml: true`.
const rawNode: Raw = { type: 'raw', value: rawHtml };
(parent as ParentWithRaw).children.splice(index, 1, rawNode);
}
}
};
};
};
18 changes: 11 additions & 7 deletions packages/jsx-email/src/renderer/render.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { htmlToText } from 'html-to-text';
import { rehype } from 'rehype';
import stringify from 'rehype-stringify';

import { type JsxEmailConfig, defineConfig, loadConfig, mergeConfig } from '../config.js';
import { callHook, callProcessHook } from '../plugins.js';
import type { PlainTextOptions, RenderOptions } from '../types.js';

import { getConditionalPlugin } from './conditional.js';
import { jsxToString } from './jsx-to-string.js';
import { getMovePlugin } from './move-style.js';
import { unescapeForRawComponent } from './raw.js';
import { getRawPlugin, unescapeForRawComponent } from './raw.js';

export const jsxEmailTags = ['jsx-email-cond'];

Expand Down Expand Up @@ -71,16 +70,24 @@ export const render = async (component: React.ReactElement, options?: RenderOpti
};

const processHtml = async (config: JsxEmailConfig, html: string) => {
const { rehype } = await import('rehype');
const { default: stringify } = await import('rehype-stringify');
const docType =
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
const movePlugin = await getMovePlugin();
const rawPlugin = await getRawPlugin();
const conditionalPlugin = await getConditionalPlugin();
const settings = { emitParseErrors: true };
const reJsxTags = new RegExp(`<[/]?(${jsxEmailTags.join('|')})>`, 'g');
// Remove any stray jsx-email markers (with or without attributes)
const reJsxTags = new RegExp(`<[/]?(${jsxEmailTags.join('|')})(?:\\s[^>]*)?>`, 'g');

// @ts-ignore: This is perfectly valid, see here: https://www.npmjs.com/package/rehype#examples
const processor = rehype().data('settings', settings);

processor.use(movePlugin);
processor.use(rawPlugin);
// Ensure conditional processing happens after raw hoisting
processor.use(conditionalPlugin);
await callProcessHook({ config, processor });

const doc = await processor
Expand All @@ -95,9 +102,6 @@ const processHtml = async (config: JsxEmailConfig, html: string) => {
let result = docType + String(doc).replace('<!doctype html>', '').replace('<head></head>', '');

result = result.replace(reJsxTags, '');
result = result.replace(/<jsx-email-raw.*?><!--(.*?)--><\/jsx-email-raw>/g, (_, p1) =>
unescapeForRawComponent(p1)
);

return result;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`Raw in Conditional > Raw in Conditional 1`] = `"<jsx-email-cond data-mso="true" data-head="true"><jsx-email-raw><!--<xml><o:OfficeDocumentSettings><o:AllowPNG /><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml>--></jsx-email-raw></jsx-email-cond>"`;

exports[`Raw in Conditional > Raw in Conditional 2`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html><head><!--[if mso]><xml><o:OfficeDocumentSettings><o:AllowPNG /><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]/--></head><body></body></html>"`;
8 changes: 4 additions & 4 deletions packages/jsx-email/test/.snapshots/conditional.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`<Conditional> component > renders expression 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html><body><!--[if lt batman]><h1>joker</h1><![endif]--></body></html>"`;
exports[`<Conditional> component > renders expression 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html><body><!--[if lt batman]><h1>joker</h1><![endif]/--></body></html>"`;

exports[`<Conditional> component > renders mso: false 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html><body><!--[if !mso]><!--><h1>batman</h1><!--<![endif]--></body></html>"`;

exports[`<Conditional> component > renders mso: true 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html><body><!--[if mso]><h1>batman</h1><![endif]--></body></html>"`;
exports[`<Conditional> component > renders mso: true 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html><body><!--[if mso]><h1>batman</h1><![endif]/--></body></html>"`;

exports[`<Conditional> component > renders with head: true 1`] = `"<head><!--[if mso]><h1>batman</h1><![endif]--></head>"`;
exports[`<Conditional> component > renders with head: true 1`] = `"<jsx-email-cond data-mso="true" data-head="true"><h1>batman</h1></jsx-email-cond>"`;

exports[`<Conditional> component > renders with jsxToString 1`] = `"<jsx-email-cond><!--[if mso]><h1>batman</h1><![endif]--></jsx-email-cond>"`;
exports[`<Conditional> component > renders with jsxToString 1`] = `"<jsx-email-cond data-mso="true"><h1>batman</h1></jsx-email-cond>"`;

exports[`<Conditional> component > throws on bad props 1`] = `[RangeError: jsx-email: Conditional expects the \`expression\` or \`mso\` prop to be defined]`;

Expand Down
2 changes: 1 addition & 1 deletion packages/jsx-email/test/.snapshots/debug.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ exports[`render > renders with debug attributes 1`] = `
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no, url=no">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes">
<meta name="x-apple-disable-message-reformatting">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no, url=no"><!--[if mso]><xml><o:OfficeDocumentSettings><o:AllowPNG></o:AllowPNG><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no, url=no"><!--[if mso]><xml><o:OfficeDocumentSettings><o:AllowPNG /><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]/-->
</head>

<body data-type="jsx-email/body" style="background-color:#ffffff;font-family:HelveticaNeue,Helvetica,Arial,sans-serif">
Expand Down
4 changes: 2 additions & 2 deletions packages/jsx-email/test/.snapshots/head.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`<Head> component > renders correctly 1`] = `"<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes"/><meta name="x-apple-disable-message-reformatting"/><meta name="format-detection" content="telephone=no, date=no, address=no, email=no, url=no"/><meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes"/><meta name="x-apple-disable-message-reformatting"/><meta name="format-detection" content="telephone=no, date=no, address=no, email=no, url=no"/><head><!--[if mso]><xml><o:OfficeDocumentSettings><o:AllowPNG></o:AllowPNG><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]--></head></head>"`;
exports[`<Head> component > renders correctly 1`] = `"<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes"/><meta name="x-apple-disable-message-reformatting"/><meta name="format-detection" content="telephone=no, date=no, address=no, email=no, url=no"/><meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes"/><meta name="x-apple-disable-message-reformatting"/><meta name="format-detection" content="telephone=no, date=no, address=no, email=no, url=no"/><jsx-email-cond data-mso="true" data-head="true"><jsx-email-raw><!--<xml><o:OfficeDocumentSettings><o:AllowPNG /><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml>--></jsx-email-raw></jsx-email-cond></head>"`;

exports[`<Head> component > renders style tags 1`] = `
"<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes"/><meta name="x-apple-disable-message-reformatting"/><meta name="format-detection" content="telephone=no, date=no, address=no, email=no, url=no"/><meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes"/><meta name="x-apple-disable-message-reformatting"/><meta name="format-detection" content="telephone=no, date=no, address=no, email=no, url=no"/><style>body{
color: red;
}</style><head><!--[if mso]><xml><o:OfficeDocumentSettings><o:AllowPNG></o:AllowPNG><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]--></head></head>"
}</style><jsx-email-cond data-mso="true" data-head="true"><jsx-email-raw><!--<xml><o:OfficeDocumentSettings><o:AllowPNG /><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml>--></jsx-email-raw></jsx-email-cond></head>"
`;
6 changes: 6 additions & 0 deletions packages/jsx-email/test/.snapshots/raw.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ exports[`<Raw> component > Should preserve content on plainText render 1`] = `"<

exports[`<Raw> component > Should render without escaping 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html><body><#if firstname & lastname>Ola!</#if></body></html>"`;

exports[`<Raw> component > Should work correctly when it has linebreaks 1`] = `
"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html><body>
Raw context
</body></html>"
`;

exports[`<Raw> component > Should work correctly with a comment as a content 1`] = `"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html><body><!--[if !mso]><!-->Ola!<!--<![endif]/--></body></html>"`;

exports[`<Raw> component > disablePlainTextOutput > Should not output to the plain text when enabled 1`] = `"Ola!"`;
Expand Down
Loading
Loading