diff --git a/modules/template-insert.js b/modules/template-insert.js index 8d092af..c87a8c1 100644 --- a/modules/template-insert.js +++ b/modules/template-insert.js @@ -122,21 +122,14 @@ export function applyVariables(text, vars, isHtml = false) { } /** - * Replace template variables in text. - * - * Supported tokens: {DATE}, {TIME}, {DATETIME}, {YEAR}, {WEEKDAY}, - * {SENDER_NAME}, {SENDER_EMAIL}, {ACCOUNT_NAME}, {ACCOUNT_EMAIL}. - * - * @param {string} text - Text containing placeholders - * @param {number} tabId - The compose tab ID (used to resolve sender identity) - * @param {boolean} isHtml - Pass true when substituting into HTML to HTML-encode identity values. - * @returns {Promise} Text with placeholders replaced - * @see applyVariables + * Resolve the sender identity variables for a compose tab by calling the + * three Thunderbird messenger APIs that require live tab/account context. + * Separating this from `replaceVariables` keeps the substitution logic pure + * and independently testable. + * @param {number} tabId + * @returns {Promise<{senderName: string, senderEmail: string, accountName: string, accountEmail: string}>} */ -export async function replaceVariables(text, tabId, isHtml = false) { - if (!text) return text; - - const now = new Date(); +export async function resolveIdentityVars(tabId) { let senderName = ""; let senderEmail = ""; let accountName = ""; @@ -168,6 +161,26 @@ export async function replaceVariables(text, tabId, isHtml = false) { console.warn("TemplateWing: could not resolve sender identity", err); } + return { senderName, senderEmail, accountName, accountEmail }; +} + +/** + * Replace template variables in text. + * + * Supported tokens: {DATE}, {TIME}, {DATETIME}, {YEAR}, {WEEKDAY}, + * {SENDER_NAME}, {SENDER_EMAIL}, {ACCOUNT_NAME}, {ACCOUNT_EMAIL}. + * + * @param {string} text - Text containing placeholders + * @param {object} vars - Pre-resolved identity vars: { senderName, senderEmail, accountName, accountEmail }. + * Obtain via {@link resolveIdentityVars} before calling this function. + * @param {boolean} isHtml - Pass true when substituting into HTML to HTML-encode identity values. + * @returns {string} Text with placeholders replaced + * @see applyVariables + * @see resolveIdentityVars + */ +export function replaceVariables(text, vars, isHtml = false) { + if (!text) return text; + const now = new Date(); return applyVariables( text, { @@ -176,10 +189,11 @@ export async function replaceVariables(text, tabId, isHtml = false) { datetime: now.toLocaleDateString() + " " + now.toLocaleTimeString(), year: now.getFullYear(), weekday: WEEKDAY_NAMES[now.getDay()], - senderName, - senderEmail, - accountName, - accountEmail, + senderName: "", + senderEmail: "", + accountName: "", + accountEmail: "", + ...vars, }, isHtml ); @@ -405,13 +419,14 @@ export async function insertTemplateIntoTab(tabId, template) { // "cursor" mode is delivered via a compose script message rather than by // rewriting the whole body, so the signature and any text the user has // already typed stay intact. + const identityVars = await resolveIdentityVars(tabId); if (resolvedBody && mode === INSERT_MODES.CURSOR) { - const body = await replaceVariables(resolvedBody, tabId, true); + const body = replaceVariables(resolvedBody, identityVars, true); const existing = await messenger.compose.getComposeDetails(tabId); const fallbackBody = await tryCursorInsert(tabId, body, existing); if (fallbackBody !== null) details.body = fallbackBody; } else if (resolvedBody) { - const body = await replaceVariables(resolvedBody, tabId, true); + const body = replaceVariables(resolvedBody, identityVars, true); if (mode === INSERT_MODES.REPLACE) { details.body = body; } else if (mode === INSERT_MODES.PREPEND) { @@ -432,7 +447,7 @@ export async function insertTemplateIntoTab(tabId, template) { } if (template.subject) { - details.subject = await replaceVariables(template.subject, tabId); + details.subject = replaceVariables(template.subject, identityVars); } if (template.to && template.to.length > 0) { diff --git a/tests/template-insert.test.js b/tests/template-insert.test.js index fae480a..69df275 100644 --- a/tests/template-insert.test.js +++ b/tests/template-insert.test.js @@ -5,8 +5,13 @@ import { installMessengerMock, uninstallMessengerMock } from "./_mock-messenger. // The module installs a storage listener at import time; install the mock first. installMessengerMock(); -const { TEMPLATE_INCLUDE_REGEX, WEEKDAY_NAMES, applyVariables, resolveNestedTemplates } = - await import("../modules/template-insert.js"); +const { + TEMPLATE_INCLUDE_REGEX, + WEEKDAY_NAMES, + applyVariables, + replaceVariables, + resolveNestedTemplates, +} = await import("../modules/template-insert.js"); after(() => uninstallMessengerMock()); @@ -118,6 +123,73 @@ describe("applyVariables", () => { }); }); +describe("replaceVariables", () => { + it("replaces {SENDER_NAME} and {SENDER_EMAIL} from provided vars", () => { + const result = replaceVariables("From: {SENDER_NAME} <{SENDER_EMAIL}>", { + senderName: "Alice", + senderEmail: "alice@example.com", + }); + assert.strictEqual(result, "From: Alice "); + }); + + it("replaces {ACCOUNT_NAME} and {ACCOUNT_EMAIL} from provided vars", () => { + const result = replaceVariables("{ACCOUNT_NAME} / {ACCOUNT_EMAIL}", { + accountName: "Work", + accountEmail: "alice.work@example.com", + }); + assert.strictEqual(result, "Work / alice.work@example.com"); + }); + + it("HTML-encodes identity values when isHtml is true", () => { + const result = replaceVariables("{SENDER_NAME}", { senderName: "Alice & Bob" }, true); + assert.strictEqual(result, "<b>Alice & Bob</b>"); + }); + + it("does not HTML-encode when isHtml is false", () => { + const result = replaceVariables("{SENDER_NAME}", { senderName: "" }, false); + assert.strictEqual(result, ""); + }); + + it("defaults identity vars to empty strings when vars is empty", () => { + const result = replaceVariables("{SENDER_NAME} <{SENDER_EMAIL}>", {}); + assert.strictEqual(result, " <>"); + }); + + it("replaces {DATE} with a non-empty string", () => { + const result = replaceVariables("{DATE}", {}); + assert.ok( + result.length > 0 && !result.includes("{DATE}"), + `{DATE} should be replaced, got: ${result}` + ); + }); + + it("replaces {TIME} with a non-empty string", () => { + const result = replaceVariables("{TIME}", {}); + assert.ok( + result.length > 0 && !result.includes("{TIME}"), + `{TIME} should be replaced, got: ${result}` + ); + }); + + it("replaces {YEAR} with a 4-digit year", () => { + const result = replaceVariables("{YEAR}", {}); + assert.match(result, /^\d{4}$/); + }); + + it("replaces {WEEKDAY} with a day name", () => { + const result = replaceVariables("{WEEKDAY}", {}); + assert.ok(WEEKDAY_NAMES.includes(result), `expected a weekday name, got: ${result}`); + }); + + it("returns null unchanged for null input", () => { + assert.strictEqual(replaceVariables(null, {}), null); + }); + + it("returns undefined unchanged for undefined input", () => { + assert.strictEqual(replaceVariables(undefined, {}), undefined); + }); +}); + describe("WEEKDAY_NAMES", () => { it("has seven English weekday names starting with Sunday", () => { assert.strictEqual(WEEKDAY_NAMES.length, 7);