Building approval emails and HtmlService notices by concatenating strings is how < in a client name becomes a broken page — or worse. fillHtmlTemplate(html, values, opts) replaces {{token}} placeholders from a values object, HTML-escapes by default, and supports a raw opt-in via {{{token}}} (triple braces) for trusted markup you already sanitized.
When to use
- Approval / reject notices, digests, and simple HtmlService UIs
- Any template where values come from a sheet (names, notes, amounts)
- Swapping a brittle string-concat email for something readable
How to use this snippet
/**
* Replace {{token}} (escaped) and {{{token}}} (raw) in an HTML string.
*
* @param {string} html
* @param {Object<string, *>} values
* @param {{onMissing?: 'blank'|'throw'}=} opts
* onMissing 'blank' (default) leaves missing keys empty;
* 'throw' fails loud when a placeholder has no value.
* @return {string}
*/
function fillHtmlTemplate(html, values, opts) {
opts = opts || {};
var onMissing = opts.onMissing || 'blank';
values = values || {};
function escapeHtml(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
return String(html).replace(
/\{\{\{([^{}]+)\}\}\}|\{\{([^{}]+)\}\}/g,
function (_, rawKey, escKey) {
var key = String(rawKey || escKey).trim();
var has = Object.prototype.hasOwnProperty.call(values, key);
if (!has) {
if (onMissing === 'throw') {
throw new Error('fillHtmlTemplate: missing key "' + key + '"');
}
return '';
}
var val = values[key];
// Triple braces: caller opts into raw HTML (must already be safe)
if (rawKey) return String(val == null ? '' : val);
return escapeHtml(val);
}
);
}
Example
var NOTICE_TMPL =
'<p>Hi {{requester}},</p>' +
'<p>Your request <strong>{{id}}</strong> was {{status}}.</p>' +
'<p>Note:</p>{{{noteHtml}}}';
function buildApprovalNotice_(row) {
return fillHtmlTemplate(NOTICE_TMPL, {
requester: row.Requester,
id: row.Id,
status: row.Status,
// Pre-escape (or build from trusted parts) before using {{{…}}}
noteHtml: '<em>' + escapeHtml(row.Note || '—') + '</em>'
}, { onMissing: 'throw' });
}
Tips:
- Default
{{token}}is always escaped — sheet cells regularly contain&,<, and quotes. - Use
{{{token}}}only for HTML you built yourself (or already ran throughescapeHtml). Never dump a raw sheet cell into triple braces. onMissing: 'blank'is fine for optional fields; use'throw'for required ones so a typo in the template fails in the editor, not in someone’s inbox.- Whitespace inside the braces is trimmed (
{{ name }}works). Nested braces aren’t supported — keep tokens flat.
Tip: NitroGAS Co-Pilot can turn a plain-English notice (“tell the requester their row was approved, include their note”) into a fillHtmlTemplate call with the right tokens.
Happy Coding!
