Skip to content
Merged
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
47 changes: 46 additions & 1 deletion packages/frontend/src/components/RichTextEditor/utils.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -169,6 +173,47 @@ describe('replaceOldTemplates', () => {
})
})

describe('substituteForPreview', () => {
it('replaces a variable span with its resolved value from varInfo', () => {
const input =
'Hi <span data-type="variable" data-id="step.ff5000f5-021c-4488-b6c2-c582c42ba3cf.hello" data-label="hello" data-value="world">{{step.ff5000f5-021c-4488-b6c2-c582c42ba3cf.hello}}</span>!'
expect(substituteForPreview(input, varInfo)).toEqual('Hi world!')
})

it('replaces a table-variable span with its resolved value', () => {
const input =
'<div data-type="tableVariable" data-id="step.ff5000f5-021c-4488-b6c2-c582c42ba3cf.hello|abcd" data-label="hello" data-value="world">{{step.ff5000f5-021c-4488-b6c2-c582c42ba3cf.hello|abcd}}</div>'
expect(substituteForPreview(input, varInfo)).toEqual('world')
})

it('substitutes legacy {{step.…}} patterns in plain text nodes', () => {
const input =
'<p>Aloha {{step.ff5000f5-021c-4488-b6c2-c582c42ba3cf.hello}}, meet {{step.ff5000f5-021c-4488-b6c2-c582c42ba3cf.papa}}.</p>'
expect(substituteForPreview(input, varInfo)).toEqual(
'<p>Aloha world, meet mama.</p>',
)
})

it('falls back to data-value when var is missing from varInfo', () => {
const input =
'<span data-type="variable" data-id="step.ff5000f5-021c-4488-b6c2-c582c42ba3cf.missing" data-label="missing" data-value="stale-fallback">{{step.ff5000f5-021c-4488-b6c2-c582c42ba3cf.missing}}</span>'
expect(substituteForPreview(input, varInfo)).toEqual('stale-fallback')
})

it('falls back to empty string when both varInfo and data-value are missing', () => {
const input =
'<span data-type="variable" data-id="step.ff5000f5-021c-4488-b6c2-c582c42ba3cf.missing">{{step.ff5000f5-021c-4488-b6c2-c582c42ba3cf.missing}}</span>'
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 = 'Lorem​Ipsum​Dolor​Sit​Amet​Co'
Expand Down
50 changes: 50 additions & 0 deletions packages/frontend/src/components/RichTextEditor/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading