diff --git a/packages/frontend/src/components/RichTextEditor/utils.test.ts b/packages/frontend/src/components/RichTextEditor/utils.test.ts index 049e39a1e..b025fe741 100644 --- a/packages/frontend/src/components/RichTextEditor/utils.test.ts +++ b/packages/frontend/src/components/RichTextEditor/utils.test.ts @@ -1,7 +1,11 @@ import escapeHTML from 'escape-html' import { describe, expect, it } from 'vitest' -import { removeProblematicWhitespace, substituteOldTemplates } from './utils' +import { + removeProblematicWhitespace, + substituteForPreview, + substituteOldTemplates, +} from './utils' const varInfo = new Map< string, @@ -169,6 +173,47 @@ describe('replaceOldTemplates', () => { }) }) +describe('substituteForPreview', () => { + it('replaces a variable span with its resolved value from varInfo', () => { + const input = + 'Hi {{step.ff5000f5-021c-4488-b6c2-c582c42ba3cf.hello}}!' + expect(substituteForPreview(input, varInfo)).toEqual('Hi world!') + }) + + it('replaces a table-variable span with its resolved value', () => { + const input = + '
Aloha {{step.ff5000f5-021c-4488-b6c2-c582c42ba3cf.hello}}, meet {{step.ff5000f5-021c-4488-b6c2-c582c42ba3cf.papa}}.
' + expect(substituteForPreview(input, varInfo)).toEqual( + 'Aloha world, meet mama.
', + ) + }) + + it('falls back to data-value when var is missing from varInfo', () => { + const input = + '{{step.ff5000f5-021c-4488-b6c2-c582c42ba3cf.missing}}' + expect(substituteForPreview(input, varInfo)).toEqual('stale-fallback') + }) + + it('falls back to empty string when both varInfo and data-value are missing', () => { + const input = + '{{step.ff5000f5-021c-4488-b6c2-c582c42ba3cf.missing}}' + expect(substituteForPreview(input, varInfo)).toEqual('') + }) + + it('returns empty string for nullish input', () => { + const inputs = [undefined, null] as unknown as string[] + for (const input of inputs) { + expect(substituteForPreview(input, varInfo)).toEqual('') + } + }) +}) + describe('removeProblematicWhitespace', () => { it('should remove non-breaking space', () => { const input = 'LoremIpsumDolorSitAmetCo' diff --git a/packages/frontend/src/components/RichTextEditor/utils.ts b/packages/frontend/src/components/RichTextEditor/utils.ts index 43de64b1c..58dd3d7b4 100644 --- a/packages/frontend/src/components/RichTextEditor/utils.ts +++ b/packages/frontend/src/components/RichTextEditor/utils.ts @@ -151,6 +151,56 @@ export function substituteOldTemplates( return substitutedDom.outerHTML } +/** + * Renders RichTextEditor HTML to plain final-form HTML, suitable for feeding + * into an email preview pipeline. + * + * - Variable / table-variable spans are replaced with their resolved value: + * prefer the entry in `varInfo`, fall back to the node's own `data-value` + * attribute (kept current by `recursiveSubstitute`). + * - Legacy `{{step.…}}` patterns remaining in plain text nodes are resolved + * via `simpleSubstitute`. + */ +export function substituteForPreview( + html: string, + varInfo: VariableInfoMap, +): string { + if (!html) { + return '' + } + + const root = parse(html) + + function processChildren(parent: NodeHTMLElement): void { + const newChildren: Node[] = [] + for (const child of parent.childNodes) { + if (child instanceof NodeHTMLElement) { + const dataType = child.getAttribute('data-type') + const dataId = child.getAttribute('data-id') + if ( + (dataType === 'variable' || dataType === 'tableVariable') && + dataId != null + ) { + const fromMap = varInfo.get(`{{${dataId}}}`)?.testRunValue + const fallback = child.getAttribute('data-value') ?? '' + newChildren.push(new TextNode(fromMap ?? fallback)) + continue + } + processChildren(child) + newChildren.push(child) + } else if (child instanceof TextNode) { + newChildren.push(new TextNode(simpleSubstitute(child.rawText, varInfo))) + } else { + newChildren.push(child) + } + } + parent.childNodes = newChildren + } + + processChildren(root) + return root.outerHTML +} + export function getPopoverPlacement( editor: Editor | null, ): PlacementWithLogical {