Regular Expressions
RegExp in SSJS — creation, flags, test, exec, and the source/global/lastIndex accessors, with the SFMC engine quirks for capture groups, lastIndex, ignoreCase, multiline, and instanceof.
Regular expressions work in SSJS using the ES3/ES5 RegExp API. Literal and constructor syntax both work, and test, String.match, String.replace (including $1 back-references and function replacers), and String.split behave as expected. A few accessors, exec’s capture-group / lastIndex behavior, instanceof RegExp, and String.search’s return index differ from the spec — those are flagged below. Every fact on this page has been proven against live SFMC CloudPage runtime output.
Status legend
| Icon | Meaning |
|---|---|
| ✅ Works | Available and behaves as expected |
| ⚠️ Partial | Available but with a documented caveat or bug |
| ❌ Missing | Not available (or undefined) — use the workaround |
Members
| Member | ES | Status | Notes |
|---|---|---|---|
RegExp.prototype.test(string) |
ES3 | ✅ Works | Most reliable way to check for a match |
RegExp.prototype.exec(string) |
ES3 | ⚠️ Partial | Full match result[0], result.index, and result.input work; capture groups result[1]+ are undefined and lastIndex does not advance |
RegExp.prototype.source |
ES3 | ✅ Works | Pattern text without slashes or flags |
RegExp.prototype.global |
ES3 | ✅ Works | true if the g flag was set |
RegExp.prototype.lastIndex |
ES3 | ⚠️ Partial | Does not advance after exec() / test() with the g flag; setting it manually is ignored |
RegExp.prototype.ignoreCase |
ES3 | ❌ Missing | undefined in SFMC — track the i flag yourself |
RegExp.prototype.multiline |
ES3 | ❌ Missing | undefined in SFMC — track the m flag yourself |
re instanceof RegExp |
ES3 | ⚠️ Partial | Always false (even for new RegExp(...)) — use re.constructor === RegExp |
Creating RegExp
// Literal syntax (preferred)
var emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
var digitsOnly = /^\d+$/;
var noSpaces = /\s/g;
// Constructor syntax (for dynamic patterns)
var fieldName = "email";
var dynamicPattern = new RegExp(fieldName + "=([^&]+)", "i");
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Chapter: Creating RegExp
*
* Proves:
* 1. Literal syntax produces a working RegExp — the chapter's three
* literals (email, digits-only, whitespace-global) each carry the
* documented pattern text and match as intended.
* 2. Constructor syntax with a dynamically assembled pattern string and a
* flag argument produces an equivalent working RegExp.
*
* EXPECTED OUTPUT: every line starts with PASS. A FAIL means the runtime no
* longer matches the documented claim and the page must be revised.
*/
function assert(id, fn, expected) {
var got;
try { got = fn(); } catch (ex) { got = "THREW: " + ex.message; }
Platform.Response.Write((got === expected ? "PASS " : "FAIL ") + id + " -> [" + got + "]\n");
}
/* 1. Literal syntax. */
var emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
var digitsOnly = /^\d+$/;
var noSpaces = /\s/g;
assert("literal emailPattern.source", function () { return String(emailPattern.source); }, "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$");
assert("literal emailPattern matches a valid address", function () { return emailPattern.test("jane@example.com") ? "true" : "false"; }, "true");
assert("literal digitsOnly.source", function () { return String(digitsOnly.source); }, "^\\d+$");
assert("literal digitsOnly matches digits", function () { return digitsOnly.test("12345") ? "true" : "false"; }, "true");
assert("literal noSpaces.source", function () { return String(noSpaces.source); }, "\\s");
assert("literal noSpaces carries the g flag", function () { return noSpaces.global ? "true" : "false"; }, "true");
/* 2. Constructor syntax with a dynamic pattern. */
var fieldName = "email";
var dynamicPattern = new RegExp(fieldName + "=([^&]+)", "i");
assert("constructor builds the dynamic source", function () { return String(dynamicPattern.source); }, "email=([^&]+)");
assert("constructor pattern matches", function () { return dynamicPattern.test("?email=jane%40x.com") ? "true" : "false"; }, "true");
assert("constructor pattern does not match unrelated text", function () { return dynamicPattern.test("?name=jane") ? "true" : "false"; }, "false");
</script>
Flags
| Flag | Meaning |
|---|---|
g |
Global — find all matches, not just the first |
i |
Case-insensitive |
m |
Multiline — ^ and $ match line boundaries |
var text = "Hello World hello";
/hello/i.test(text); // true (case-insensitive)
text.match(/hello/gi); // ["Hello", "hello"] (global + case-insensitive)
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Chapter: Flags
*
* Proves, for the chapter's flag table and its worked example:
* 1. g — String.match with the g flag returns ALL matches, not just the
* first; without g it returns only the first match.
* 2. i — /hello/i.test("Hello World hello") is true (case-insensitive),
* while /hello/ without the flag does not match "Hello".
* 3. m — ^ and $ match line boundaries when the m flag is set, and do not
* when it is absent.
* 4. The chapter's example: text.match(/hello/gi) yields
* ["Hello", "hello"] — length 2, elements in source order.
*
* EXPECTED OUTPUT: every line starts with PASS. A FAIL means the runtime no
* longer matches the documented claim and the page must be revised.
*/
function assert(id, fn, expected) {
var got;
try { got = fn(); } catch (ex) { got = "THREW: " + ex.message; }
Platform.Response.Write((got === expected ? "PASS " : "FAIL ") + id + " -> [" + got + "]\n");
}
var text = "Hello World hello";
/* 2. i — case-insensitive. */
assert("/hello/i.test(text) is true", function () { return /hello/i.test(text) ? "true" : "false"; }, "true");
assert("/hello/ (no i) does not match 'Hello' alone", function () { return /hello/.test("Hello") ? "true" : "false"; }, "false");
assert("/hello/i matches 'Hello'", function () { return /hello/i.test("Hello") ? "true" : "false"; }, "true");
/* 1. + 4. g — all matches; the chapter's example. */
assert("text.match(/hello/gi).length is 2", function () { return String(text.match(/hello/gi).length); }, "2");
assert("text.match(/hello/gi)[0] is 'Hello'", function () { return String(text.match(/hello/gi)[0]); }, "Hello");
assert("text.match(/hello/gi)[1] is 'hello'", function () { return String(text.match(/hello/gi)[1]); }, "hello");
assert("without g, match returns only the first match", function () { return String(text.match(/hello/i)[0]); }, "Hello");
/* 3. m — ^ and $ match line boundaries. */
var lines = "alpha\nbeta";
assert("/^beta/m matches a later line", function () { return /^beta/m.test(lines) ? "true" : "false"; }, "true");
assert("/^beta/ without m does not match a later line", function () { return /^beta/.test(lines) ? "true" : "false"; }, "false");
assert("/alpha$/m matches an earlier line end", function () { return /alpha$/m.test(lines) ? "true" : "false"; }, "true");
assert("/alpha$/ without m does not match an earlier line end", function () { return /alpha$/.test(lines) ? "true" : "false"; }, "false");
</script>
test
(ES3) — ✅ Works. Returns true if the pattern matches the string, false otherwise. This is the most reliable way to detect a match in SFMC.
var email = "jane@example.com";
var emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (emailRe.test(email)) {
Write("Valid email");
} else {
Write("Invalid email");
}
Platform.Function.IsEmailAddress()is usually more reliable than a custom regex for SFMC email validation.
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Chapter: test
*
* Proves:
* 1. RegExp.prototype.test returns a PRIMITIVE boolean (typeof "boolean").
* 2. It returns true when the pattern matches and false when it does not —
* the chapter's email example, both branches.
* 3. The result drives an if/else exactly as the chapter's example shows.
* 4. The chapter's note that Platform.Function.IsEmailAddress() is the
* more reliable SFMC email check: it agrees with the regex on a valid
* address and rejects the invalid one.
*
* EXPECTED OUTPUT: every line starts with PASS. A FAIL means the runtime no
* longer matches the documented claim and the page must be revised.
*/
function assert(id, fn, expected) {
var got;
try { got = fn(); } catch (ex) { got = "THREW: " + ex.message; }
Platform.Response.Write((got === expected ? "PASS " : "FAIL ") + id + " -> [" + got + "]\n");
}
var emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
var email = "jane@example.com";
var badEmail = "not an email";
/* 1. The result is a primitive boolean. */
assert("typeof test() result is boolean", function () { return String(typeof emailRe.test(email)); }, "boolean");
assert("test() === true on a match", function () { return (emailRe.test(email) === true) ? "true" : "false"; }, "true");
assert("test() === false on a non-match", function () { return (emailRe.test(badEmail) === false) ? "true" : "false"; }, "true");
/* 2. Positive and negative results. */
assert("emailRe.test('jane@example.com') is true", function () { return emailRe.test(email) ? "true" : "false"; }, "true");
assert("emailRe.test('not an email') is false", function () { return emailRe.test(badEmail) ? "true" : "false"; }, "false");
assert("emailRe.test('jane@example') is false (no TLD dot)", function () { return emailRe.test("jane@example") ? "true" : "false"; }, "false");
/* 3. The chapter's if/else example. */
function classify(value) {
if (emailRe.test(value)) { return "Valid email"; }
return "Invalid email";
}
assert("example writes 'Valid email' for a valid address", function () { return classify(email); }, "Valid email");
assert("example writes 'Invalid email' for an invalid address", function () { return classify(badEmail); }, "Invalid email");
/* 4. The recommended Platform.Function.IsEmailAddress alternative. */
assert("Platform.Function.IsEmailAddress accepts the valid address", function () { return Platform.Function.IsEmailAddress(email) ? "true" : "false"; }, "true");
assert("Platform.Function.IsEmailAddress rejects the invalid address", function () { return Platform.Function.IsEmailAddress(badEmail) ? "true" : "false"; }, "false");
</script>
exec
(ES3) — ⚠️ Partial. exec returns an array whose result[0] (full match), result.index, and result.input are all correct, but capture groups result[1] and beyond are undefined, and lastIndex does not advance, so the usual while ((m = re.exec(str)) !== null) loop never terminates with the g flag. Note that result.length is always 3 regardless of how many groups the pattern has, so it cannot be used to count captures. To collect all matches or read capture groups, use String.match instead.
RegExp.exec capture groups (result[1]+) are undefined in SFMC SSJS, and lastIndex does not advance with the g flag — do not loop on exec(). Use String.match(/.../g) to collect all matches.
// ❌ Do NOT do this — lastIndex never advances, this loops forever:
// var re = /[a-z]+(\d+)/g;
// while ((match = re.exec(text)) !== null) { ... }
// ✅ Collect all matches with String.match + the g flag:
var text = "foo123 bar456 baz789";
var matches = text.match(/[a-z]+\d+/g);
// ["foo123", "bar456", "baz789"]
// ✅ Read a single match's full text (result[0] is reliable):
var first = /[a-z]+\d+/.exec(text);
Write(first[0]); // "foo123"
// first[1] is undefined in SFMC even though the pattern has a group
To extract capture groups reliably, match the non-global pattern with String.match:
var dateMatch = "2026-06-18".match(/(\d{4})-(\d{2})-(\d{2})/);
// dateMatch[1] = "2026", dateMatch[2] = "06", dateMatch[3] = "18"
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Chapter: exec
*
* Proves:
* 1. exec returns an array-like result whose result[0] (full match),
* result.index and result.input are all correct.
* 2. DEVIATION marked "DEV": capture groups result[1] and beyond are
* undefined (spec: the captured substrings).
* 3. DEVIATION marked "DEV": result.length is always 3, regardless of how
* many groups the pattern declares (spec: 1 + number of groups), so it
* cannot be used to count captures.
* 4. DEVIATION marked "DEV": lastIndex does not advance after exec() with
* the g flag (spec: it advances past the match), which is why the usual
* while ((m = re.exec(s)) !== null) loop never terminates.
* 5. The documented workarounds really work:
* - String.match(/.../g) collects every match
* - String.match with a non-global pattern exposes capture groups
* (the chapter's "2026-06-18" date example)
* - reading result[0] from a single exec is reliable
*
* NOT ASSERTED:
* - The non-terminating while-loop itself. A hanging CloudPage returns no
* response at all, so the hang can never surface as a PASS line; the
* non-advancing lastIndex that causes it is asserted instead (point 4).
*
* EXPECTED OUTPUT: every line starts with PASS. A FAIL means the runtime no
* longer matches the documented claim and the page must be revised.
*/
function assert(id, fn, expected) {
var got;
try { got = fn(); } catch (ex) { got = "THREW: " + ex.message; }
Platform.Response.Write((got === expected ? "PASS " : "FAIL ") + id + " -> [" + got + "]\n");
}
var text = "foo123 bar456 baz789";
/* 1. result[0], result.index and result.input are correct. */
var first = /[a-z]+\d+/.exec(text);
assert("exec result[0] is the full match", function () { return String(first[0]); }, "foo123");
assert("exec result.index is 0", function () { return String(first.index); }, "0");
assert("exec result.input is the subject string", function () { return String(first.input); }, "foo123 bar456 baz789");
var second = /bar\d+/.exec(text);
assert("exec result.index reports a later offset", function () { return String(second.index); }, "7");
assert("exec result[0] for the later match", function () { return String(second[0]); }, "bar456");
/* 2. DEVIATION — capture groups are undefined. */
var grouped = /[a-z]+(\d+)/.exec(text);
assert("DEV exec result[0] still correct with a group", function () { return String(grouped[0]); }, "foo123");
assert("DEV exec result[1] is undefined (spec: '123')", function () { return String(typeof grouped[1]); }, "undefined");
var twoGroups = /([a-z]+)(\d+)/.exec(text);
assert("DEV exec result[1] undefined with two groups (spec: 'foo')", function () { return String(typeof twoGroups[1]); }, "undefined");
assert("DEV exec result[2] undefined with two groups (spec: '123')", function () { return String(typeof twoGroups[2]); }, "undefined");
/* 3. DEVIATION — result.length is always 3. */
var noGroup = /[a-z]+\d+/.exec(text);
assert("DEV exec result.length is 3 with no groups (spec: 1)", function () { return String(noGroup.length); }, "3");
assert("DEV exec result.length is 3 with one group (spec: 2)", function () { return String(grouped.length); }, "3");
assert("DEV exec result.length is 3 with two groups (spec: 3)", function () { return String(twoGroups.length); }, "3");
/* 4. DEVIATION — lastIndex does not advance. */
var re = /[a-z]+\d+/g;
assert("lastIndex starts at 0", function () { return String(re.lastIndex); }, "0");
re.exec(text);
assert("DEV lastIndex still 0 after exec with g (spec: 6)", function () { return String(re.lastIndex); }, "0");
assert("DEV a second exec returns the SAME match (spec: 'bar456')", function () { return String(re.exec(text)[0]); }, "foo123");
/* 5. Workarounds. */
var matches = text.match(/[a-z]+\d+/g);
assert("workaround String.match(g) length is 3", function () { return String(matches.length); }, "3");
assert("workaround String.match(g)[0]", function () { return String(matches[0]); }, "foo123");
assert("workaround String.match(g)[1]", function () { return String(matches[1]); }, "bar456");
assert("workaround String.match(g)[2]", function () { return String(matches[2]); }, "baz789");
var dateMatch = "2026-06-18".match(/(\d{4})-(\d{2})-(\d{2})/);
assert("workaround match capture group 1 is the year", function () { return String(dateMatch[1]); }, "2026");
assert("workaround match capture group 2 is the month", function () { return String(dateMatch[2]); }, "06");
assert("workaround match capture group 3 is the day", function () { return String(dateMatch[3]); }, "18");
</script>
source
(ES3) — ✅ Works. Returns the pattern text, excluding the surrounding slashes and any flags.
var re = /\d{4}-\d{2}-\d{2}/g;
re.source; // "\\d{4}-\\d{2}-\\d{2}"
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Chapter: source
*
* Proves:
* 1. RegExp.prototype.source returns the pattern text, excluding the
* surrounding slashes and any flags — the chapter's date example
* yields "\d{4}-\d{2}-\d{2}".
* 2. The value is a primitive string (typeof "string").
* 3. The flags are genuinely excluded: the same pattern with and without
* the g flag reports an identical source.
* 4. A constructor-built RegExp reports the pattern string it was given.
*
* EXPECTED OUTPUT: every line starts with PASS. A FAIL means the runtime no
* longer matches the documented claim and the page must be revised.
*/
function assert(id, fn, expected) {
var got;
try { got = fn(); } catch (ex) { got = "THREW: " + ex.message; }
Platform.Response.Write((got === expected ? "PASS " : "FAIL ") + id + " -> [" + got + "]\n");
}
var re = /\d{4}-\d{2}-\d{2}/g;
/* 1. + 2. The documented value and its type. */
assert("re.source excludes slashes and flags", function () { return String(re.source); }, "\\d{4}-\\d{2}-\\d{2}");
assert("typeof re.source is string", function () { return String(typeof re.source); }, "string");
/* 3. The flags are not part of source. */
var reNoFlag = /\d{4}-\d{2}-\d{2}/;
assert("source is identical with and without the g flag", function () { return (re.source === reNoFlag.source) ? "true" : "false"; }, "true");
/* 4. Constructor form. */
var built = new RegExp("a+b");
assert("constructor RegExp reports its pattern string", function () { return String(built.source); }, "a+b");
</script>
global
(ES3) — ✅ Works. true if the g flag was set on the RegExp, false otherwise.
/abc/g.global; // true
/abc/.global; // false
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Chapter: global
*
* Proves:
* 1. RegExp.prototype.global is true when the g flag was set —
* /abc/g.global is true.
* 2. It is false when the g flag was not set — /abc/.global is false.
* 3. The value is a primitive boolean (typeof "boolean"), not undefined,
* which is what distinguishes it from the missing ignoreCase and
* multiline accessors documented further down the page.
* 4. The flag is still reported when combined with other flags.
*
* EXPECTED OUTPUT: every line starts with PASS. A FAIL means the runtime no
* longer matches the documented claim and the page must be revised.
*/
function assert(id, fn, expected) {
var got;
try { got = fn(); } catch (ex) { got = "THREW: " + ex.message; }
Platform.Response.Write((got === expected ? "PASS " : "FAIL ") + id + " -> [" + got + "]\n");
}
/* 1. + 2. The two documented results. */
assert("/abc/g.global is true", function () { return /abc/g.global ? "true" : "false"; }, "true");
assert("/abc/.global is false", function () { return /abc/.global ? "true" : "false"; }, "false");
/* 3. It is a real boolean, not undefined. */
assert("typeof /abc/g.global is boolean", function () { return String(typeof /abc/g.global); }, "boolean");
assert("typeof /abc/.global is boolean", function () { return String(typeof /abc/.global); }, "boolean");
assert("/abc/g.global === true", function () { return (/abc/g.global === true) ? "true" : "false"; }, "true");
assert("/abc/.global === false", function () { return (/abc/.global === false) ? "true" : "false"; }, "true");
/* 4. Combined with another flag. */
assert("/abc/gi.global is true", function () { return /abc/gi.global ? "true" : "false"; }, "true");
</script>
lastIndex
(ES3) — ⚠️ Partial. The property exists but does not advance after exec() / test() with the g flag, so it cannot be used to drive stateful iteration. Use String.match(/.../g) to get all matches in one call.
var re = /\d+/g;
re.exec("a1 b2 c3");
re.lastIndex; // stays 0 in SFMC — does not advance
// Setting lastIndex manually is also ignored — the next exec still
// matches from the start:
re.lastIndex = 3;
re.exec("a1 b2 c3"); // still matches "1" at index 1, not from position 3
// Use String.match instead to get every match:
"a1 b2 c3".match(/\d+/g); // ["1", "2", "3"]
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Chapter: lastIndex
*
* Proves:
* 1. The property exists and starts at 0.
* 2. DEVIATION marked "DEV": it does NOT advance after exec() with the g
* flag (spec: it advances to the index just past the match), so it
* cannot drive stateful iteration.
* 3. DEVIATION marked "DEV": it does NOT advance after test() with the g
* flag either (spec: same advance as exec).
* 4. DEVIATION marked "DEV": setting lastIndex manually is ignored — the
* next exec still matches from the start of the string (spec: matching
* resumes at the assigned offset).
* 5. The documented workaround: String.match(/.../g) returns every match
* in one call — the chapter's "a1 b2 c3" example yields ["1","2","3"].
*
* EXPECTED OUTPUT: every line starts with PASS. A FAIL means the runtime no
* longer matches the documented claim and the page must be revised.
*/
function assert(id, fn, expected) {
var got;
try { got = fn(); } catch (ex) { got = "THREW: " + ex.message; }
Platform.Response.Write((got === expected ? "PASS " : "FAIL ") + id + " -> [" + got + "]\n");
}
var subject = "a1 b2 c3";
/* 1. The property exists and starts at 0. */
var re = /\d+/g;
assert("lastIndex exists and is a number", function () { return String(typeof re.lastIndex); }, "number");
assert("lastIndex starts at 0", function () { return String(re.lastIndex); }, "0");
/* 2. DEVIATION — exec does not advance it. */
re.exec(subject);
assert("DEV lastIndex stays 0 after exec with g (spec: 2)", function () { return String(re.lastIndex); }, "0");
/* 3. DEVIATION — test does not advance it either. */
var reTest = /\d+/g;
reTest.test(subject);
assert("DEV lastIndex stays 0 after test with g (spec: 2)", function () { return String(reTest.lastIndex); }, "0");
/* 4. DEVIATION — assigning lastIndex is ignored. */
var reSet = /\d+/g;
reSet.lastIndex = 3;
assert("DEV exec after lastIndex=3 still matches '1' (spec: '2')", function () { return String(reSet.exec(subject)[0]); }, "1");
assert("DEV exec after lastIndex=3 still reports index 1 (spec: 4)", function () { return String(reSet.exec(subject).index); }, "1");
/* 5. Workaround — String.match with the g flag. */
var all = subject.match(/\d+/g);
assert("workaround match(/\\d+/g).length is 3", function () { return String(all.length); }, "3");
assert("workaround match(/\\d+/g)[0]", function () { return String(all[0]); }, "1");
assert("workaround match(/\\d+/g)[1]", function () { return String(all[1]); }, "2");
assert("workaround match(/\\d+/g)[2]", function () { return String(all[2]); }, "3");
</script>
instanceof
(ES3) — ⚠️ Partial. In the SFMC engine, re instanceof RegExp is always false — even for objects created with new RegExp(...). This differs from standard JavaScript, where it returns true. Unlike Function (where .constructor is broken but instanceof works), for RegExp the reverse holds: re.constructor === RegExp correctly returns true. Use the constructor comparison to detect a RegExp.
var re = new RegExp("\\d+");
re instanceof RegExp; // false in SFMC (true in standard JS)
re.constructor === RegExp; // true — use this instead
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Chapter: instanceof
*
* Proves:
* 1. DEVIATION marked "DEV": re instanceof RegExp is ALWAYS false in the
* SFMC engine (spec / standard JS: true), including for objects built
* with new RegExp(...) and for literal RegExps.
* 2. The documented workaround works: re.constructor === RegExp is
* correctly true — for both the constructor form and the literal form.
* This is the opposite of Function, where .constructor is broken but
* instanceof works.
* 3. The constructor comparison also discriminates: a plain object's
* constructor is not RegExp.
*
* EXPECTED OUTPUT: every line starts with PASS. A FAIL means the runtime no
* longer matches the documented claim and the page must be revised.
*/
function assert(id, fn, expected) {
var got;
try { got = fn(); } catch (ex) { got = "THREW: " + ex.message; }
Platform.Response.Write((got === expected ? "PASS " : "FAIL ") + id + " -> [" + got + "]\n");
}
var re = new RegExp("\\d+");
var reLiteral = /\d+/;
/* 1. DEVIATION — instanceof is always false. */
assert("DEV new RegExp() instanceof RegExp is false (spec: true)", function () { return (re instanceof RegExp) ? "true" : "false"; }, "false");
assert("DEV literal instanceof RegExp is false (spec: true)", function () { return (reLiteral instanceof RegExp) ? "true" : "false"; }, "false");
/* 2. Workaround — the constructor comparison is correct. */
assert("workaround new RegExp().constructor === RegExp is true", function () { return (re.constructor === RegExp) ? "true" : "false"; }, "true");
assert("workaround literal .constructor === RegExp is true", function () { return (reLiteral.constructor === RegExp) ? "true" : "false"; }, "true");
/* 3. The workaround still discriminates. */
var notRe = {};
assert("plain object .constructor === RegExp is false", function () { return (notRe.constructor === RegExp) ? "true" : "false"; }, "false");
</script>
ignoreCase
(ES3) — ❌ Missing. RegExp.prototype.ignoreCase is undefined in SFMC. If you need to know whether the i flag is active, track it yourself when you construct the RegExp.
// ignoreCase is undefined in SFMC — store the flag separately:
var caseInsensitive = true;
var re = caseInsensitive ? /hello/i : /hello/;
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Chapter: ignoreCase
*
* Proves:
* 1. DEVIATION marked "DEV": RegExp.prototype.ignoreCase is undefined in
* SFMC (spec: true when the i flag is set, false otherwise) — for both
* a pattern WITH the i flag and one without it.
* 2. Reading the missing accessor does not throw.
* 3. The i flag itself still works even though it cannot be read back,
* which is why the chapter's advice is to track the flag yourself
* rather than to avoid the flag.
* 4. The documented workaround: keep a separate variable and pick the
* RegExp from it — both branches produce the intended matcher.
*
* EXPECTED OUTPUT: every line starts with PASS. A FAIL means the runtime no
* longer matches the documented claim and the page must be revised.
*/
function assert(id, fn, expected) {
var got;
try { got = fn(); } catch (ex) { got = "THREW: " + ex.message; }
Platform.Response.Write((got === expected ? "PASS " : "FAIL ") + id + " -> [" + got + "]\n");
}
/* 1. + 2. DEVIATION — the accessor is undefined and safe to read. */
assert("DEV /hello/i.ignoreCase is undefined (spec: true)", function () { return String(typeof /hello/i.ignoreCase); }, "undefined");
assert("DEV /hello/.ignoreCase is undefined (spec: false)", function () { return String(typeof /hello/.ignoreCase); }, "undefined");
assert("DEV new RegExp('h','i').ignoreCase is undefined (spec: true)", function () { return String(typeof new RegExp("h", "i").ignoreCase); }, "undefined");
/* 3. The flag still works at match time. */
assert("the i flag still matches case-insensitively", function () { return /hello/i.test("HELLO") ? "true" : "false"; }, "true");
assert("without the i flag the match is case-sensitive", function () { return /hello/.test("HELLO") ? "true" : "false"; }, "false");
/* 4. Workaround — track the flag in your own variable. */
var caseInsensitive = true;
var re = caseInsensitive ? /hello/i : /hello/;
assert("workaround flag=true yields a case-insensitive matcher", function () { return re.test("Hello") ? "true" : "false"; }, "true");
var caseSensitive = false;
var re2 = caseSensitive ? /hello/i : /hello/;
assert("workaround flag=false yields a case-sensitive matcher", function () { return re2.test("Hello") ? "true" : "false"; }, "false");
assert("workaround variable itself is readable", function () { return caseInsensitive ? "true" : "false"; }, "true");
</script>
multiline
(ES3) — ❌ Missing. RegExp.prototype.multiline is undefined in SFMC. Track the m flag yourself when constructing the RegExp if you need to read it back.
// multiline is undefined in SFMC — store the flag separately:
var isMultiline = true;
var re = isMultiline ? /^line/m : /^line/;
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Chapter: multiline
*
* Proves:
* 1. DEVIATION marked "DEV": RegExp.prototype.multiline is undefined in
* SFMC (spec: true when the m flag is set, false otherwise) — for both
* a pattern WITH the m flag and one without it.
* 2. Reading the missing accessor does not throw.
* 3. The m flag itself still works even though it cannot be read back.
* 4. The documented workaround: keep a separate variable and pick the
* RegExp from it — both branches produce the intended matcher.
*
* EXPECTED OUTPUT: every line starts with PASS. A FAIL means the runtime no
* longer matches the documented claim and the page must be revised.
*/
function assert(id, fn, expected) {
var got;
try { got = fn(); } catch (ex) { got = "THREW: " + ex.message; }
Platform.Response.Write((got === expected ? "PASS " : "FAIL ") + id + " -> [" + got + "]\n");
}
var lines = "alpha\nline two";
/* 1. + 2. DEVIATION — the accessor is undefined and safe to read. */
assert("DEV /^line/m.multiline is undefined (spec: true)", function () { return String(typeof /^line/m.multiline); }, "undefined");
assert("DEV /^line/.multiline is undefined (spec: false)", function () { return String(typeof /^line/.multiline); }, "undefined");
assert("DEV new RegExp('^line','m').multiline is undefined (spec: true)", function () { return String(typeof new RegExp("^line", "m").multiline); }, "undefined");
/* 3. The flag still works at match time. */
assert("the m flag still matches a later line start", function () { return /^line/m.test(lines) ? "true" : "false"; }, "true");
assert("without the m flag the later line start does not match", function () { return /^line/.test(lines) ? "true" : "false"; }, "false");
/* 4. Workaround — track the flag in your own variable. */
var isMultiline = true;
var re = isMultiline ? /^line/m : /^line/;
assert("workaround flag=true yields a multiline matcher", function () { return re.test(lines) ? "true" : "false"; }, "true");
var notMultiline = false;
var re2 = notMultiline ? /^line/m : /^line/;
assert("workaround flag=false yields a single-line matcher", function () { return re2.test(lines) ? "true" : "false"; }, "false");
assert("workaround variable itself is readable", function () { return isMultiline ? "true" : "false"; }, "true");
</script>
Common Patterns
// Email validation (basic)
var isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
// Digits only
var isNumeric = /^\d+$/.test(value);
// Alphanumeric
var isAlphanumeric = /^[a-zA-Z0-9]+$/.test(value);
// UUID / GUID
var isGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
// URL query parameter extraction
function getQueryParam(url, name) {
var re = new RegExp("[?&]" + name + "=([^&]*)");
var match = url.match(re);
return match ? decodeURIComponent(match[1]) : null;
}
// Escape HTML special characters
function escapeHtml(str) {
return str.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
Show test script
<script runat="server">
Platform.Load("core", "1.1.5");
/*
* Chapter: Common Patterns
*
* Proves that each ready-made snippet in the chapter really behaves as
* advertised:
* 1. The basic email test accepts a valid address and rejects an invalid
* one.
* 2. The digits-only test accepts digits and rejects mixed input.
* 3. The alphanumeric test accepts letters+digits and rejects punctuation.
* 4. The UUID / GUID test accepts a well-formed GUID in either case (the
* i flag) and rejects a malformed one.
* 5. getQueryParam() extracts and URI-decodes a named parameter, and
* returns null when the parameter is absent — note it relies on
* String.match capture groups, which work (unlike exec's).
* 6. escapeHtml() replaces all five special characters, including repeat
* occurrences via the g flag.
*
* EXPECTED OUTPUT: every line starts with PASS. A FAIL means the runtime no
* longer matches the documented claim and the page must be revised.
*/
function assert(id, fn, expected) {
var got;
try { got = fn(); } catch (ex) { got = "THREW: " + ex.message; }
Platform.Response.Write((got === expected ? "PASS " : "FAIL ") + id + " -> [" + got + "]\n");
}
/* 1. Email validation (basic). */
assert("isEmail accepts a valid address", function () { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test("jane@example.com") ? "true" : "false"; }, "true");
assert("isEmail rejects a string without @", function () { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test("jane.example.com") ? "true" : "false"; }, "false");
/* 2. Digits only. */
assert("isNumeric accepts digits", function () { return /^\d+$/.test("12345") ? "true" : "false"; }, "true");
assert("isNumeric rejects mixed input", function () { return /^\d+$/.test("12a45") ? "true" : "false"; }, "false");
/* 3. Alphanumeric. */
assert("isAlphanumeric accepts letters and digits", function () { return /^[a-zA-Z0-9]+$/.test("abc123XYZ") ? "true" : "false"; }, "true");
assert("isAlphanumeric rejects punctuation", function () { return /^[a-zA-Z0-9]+$/.test("abc-123") ? "true" : "false"; }, "false");
/* 4. UUID / GUID. */
var guidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
assert("isGuid accepts a lowercase GUID", function () { return guidRe.test("3f2504e0-4f89-11d3-9a0c-0305e82c3301") ? "true" : "false"; }, "true");
assert("isGuid accepts an uppercase GUID (i flag)", function () { return guidRe.test("3F2504E0-4F89-11D3-9A0C-0305E82C3301") ? "true" : "false"; }, "true");
assert("isGuid rejects a malformed GUID", function () { return guidRe.test("3f2504e0-4f89-11d3-9a0c") ? "true" : "false"; }, "false");
/* 5. URL query parameter extraction. */
function getQueryParam(url, name) {
var re = new RegExp("[?&]" + name + "=([^&]*)");
var match = url.match(re);
return match ? decodeURIComponent(match[1]) : null;
}
assert("getQueryParam reads the first parameter", function () { return String(getQueryParam("https://x.test/p?id=42&mode=edit", "id")); }, "42");
assert("getQueryParam reads a later parameter", function () { return String(getQueryParam("https://x.test/p?id=42&mode=edit", "mode")); }, "edit");
assert("getQueryParam URI-decodes the value", function () { return String(getQueryParam("https://x.test/p?email=jane%40example.com", "email")); }, "jane@example.com");
assert("getQueryParam returns null for a missing parameter", function () { return String(getQueryParam("https://x.test/p?id=42", "nope")); }, "null");
/* 6. Escape HTML special characters. */
function escapeHtml(str) {
return str.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
assert("escapeHtml escapes an ampersand", function () { return String(escapeHtml("a & b")); }, "a & b");
assert("escapeHtml escapes angle brackets", function () { return String(escapeHtml("<b>")); }, "<b>");
assert("escapeHtml escapes a double quote", function () { return String(escapeHtml("say \"hi\"")); }, "say "hi"");
assert("escapeHtml escapes a single quote", function () { return String(escapeHtml("it's")); }, "it's");
assert("escapeHtml escapes every occurrence (g flag)", function () { return String(escapeHtml("a<b<c")); }, "a<b<c");
assert("escapeHtml escapes ampersands before the entities it inserts", function () { return String(escapeHtml("&<")); }, "&<");
</script>