@mikezimm/fps-core-v7
Version:
Library of reusable core interfaces, types and constants migrated from fps-library-v2
154 lines • 8.54 kB
JavaScript
import { HandleBarsRegex } from "./HandleBarsRegex";
/**
* Replaces `{{key}}` handlebars in a template string with matching property values from `item`.
*
* Originally handled only two replacements. Refactored to support any number of handlebars,
* specifically to support Drilldown `linkSubstitute` where multiple field values are substituted
* into a single URL string (e.g. to pre-fill a SharePoint form via query parameters).
*
* Key normalization: each `{{key}}` is trimmed and any `/` characters are removed before
* looking up the value on `item`. This is consistent with how Drilldown handles lookup columns
* (e.g. `{{AssignedTo/Title}}` becomes `AssignedToTitle`).
*
* NOTE: A similar general-purpose function exists at src/logic/Strings/handleBarsHTML.tsx
* (`replaceHTMLHandleBars`) — consider that for non-URL use cases.
*
* @param item - The list item object whose properties fill the handlebars.
* Values are coerced to string via String(). Null/undefined are treated as missing.
*
* @param handleBarString - The template string containing one or more `{{key}}` placeholders.
* Example: `"https://example.com?Title={{Title}}&Category={{Category}}"`
*
* @param emptyIfSubEmpty - Legacy parameter. When true and any substitution value is missing,
* the entire result is returned as `''`.
* Equivalent to passing `emptyHandling = 'omitAll'`.
* Ignored when `emptyHandling` is set.
*
* @param maxReplacements - Optional. Limits how many handlebars are substituted, left to right.
* Any handlebars beyond the limit are left intact as `{{key}}`.
* Omit or pass undefined to replace all handlebars.
*
* @param emptyHandling - Optional. Controls what happens when a value is missing/null/undefined.
* Each slot is handled independently — only the missing ones are affected.
*
* 'omitAll' - If ANY value is missing, return `''` for the whole string.
* Explicit equivalent of `emptyIfSubEmpty = true`.
*
* 'empty' - Substitute missing values with `''`, leaving the placeholder param in the result.
* Example: `&Category=` (empty value, param stays).
* Safe for use with URLs and plain text strings.
*
* 'omit' - Substitute missing values with `''`, then strip those entire query parameters
* from the URL result. Intended for URL strings only.
* Example: `&Category=` is removed entirely.
* Only slots with missing values are removed — all others remain intact.
*
* 'preserve' - Leave the original `{{key}}` handlebars unchanged for any missing values.
* Useful when the template may be re-processed later, or when you want
* to return the original string unchanged for unmatched fields.
*
* default - When no `emptyHandling` is set and `emptyIfSubEmpty` is false,
* behaves the same as 'preserve': missing values keep their `{{key}}` intact.
*
* @returns The substituted string, or `''` if `emptyIfSubEmpty`/`'omitAll'` is triggered.
* Returns `handleBarString` unchanged if it is not a string type.
*/
export function replaceHandleBarsValues(item, handleBarString, emptyIfSubEmpty, maxReplacements, emptyHandling) {
/**
* Implementation notes:
* - `handleBarString` is split by `HandleBarsRegex` which captures the key names.
* Even indices in the resulting array are literal text; odd indices are the captured keys.
* - Each part is pushed into `finalParts` in order and joined at the end.
* - For 'omit' mode, a null-character sentinel is pushed for missing values so the
* post-join cleanup step can strip the entire "&key=" URL segment precisely,
* without touching other parts of the string.
*/
if (typeof handleBarString !== 'string') {
return handleBarString;
}
// Flag to indicate we should return an empty string (legacy emptyIfSubEmpty / omitAll behavior)
let returnEmpty = false;
// Determine max replacements: if param is a non-negative number use it, otherwise replace all
const maxRepls = (typeof maxReplacements === 'number' && maxReplacements >= 0) ? Math.floor(maxReplacements) : Infinity;
let replacedCount = 0;
// Sentinel used by 'omit' mode to mark empty params for cleanup after joining
// Using a null character — safe since it will never appear in a real URL or value
const OMIT_SENTINEL = '\u0000OMIT';
// Split into text and capture groups. Example: "a{{One}}b{{Two}}c" -> ["a","One","b","Two","c"]
const linkSplits = handleBarString.split(HandleBarsRegex);
// Build the result array by pushing each part in order
const finalParts = [];
linkSplits.forEach((split, idx) => {
// Even indices are literal text (base URL, "&key=", etc.) — always push as-is
if (idx % 2 === 0) {
finalParts.push(split);
return;
}
// Odd indices are the captured keys from the regex
// Raw key may include lookup slashes; normalize by trimming and removing all '/'
const rawKey = split || '';
const normalizedKey = rawKey.trim().replace(/\//g, '');
// If we've already done the maximum allowed replacements, leave the handlebars intact
if (replacedCount >= maxRepls) {
finalParts.push(`{{${rawKey}}}`);
replacedCount += 1;
return;
}
// Check whether the item has a real (non-null, non-undefined) value for this key
const hasValue = item
&& Object.prototype.hasOwnProperty.call(item, normalizedKey)
&& item[normalizedKey] !== undefined
&& item[normalizedKey] !== null;
if (hasValue) {
// Item has a real value — use it
finalParts.push(String(item[normalizedKey]));
}
else if (emptyHandling === 'omit') {
// Push sentinel — the cleanup step below will strip the entire "&key=SENTINEL" segment
finalParts.push(OMIT_SENTINEL);
}
else if (emptyHandling === 'empty') {
// Keep the param in the URL but set the value to empty (e.g. &Category=)
finalParts.push('');
}
else if (emptyHandling === 'preserve') {
// Leave the original handlebars intact so the template is returned unchanged for this slot
finalParts.push(`{{${rawKey}}}`);
}
else if (emptyHandling === 'omitAll' || emptyIfSubEmpty) {
// Legacy / omitAll: flag that the whole result should be returned as ''
returnEmpty = true;
finalParts.push('');
}
else {
// Default fallback: preserve the original {{key}} handlebars so the template is returned intact
finalParts.push(`{{${rawKey}}}`);
}
replacedCount += 1;
});
// Short-circuit: one or more missing values triggered the whole-string empty rule
if (returnEmpty) {
return '';
}
const joined = finalParts.join('');
// 'omit' mode: strip any query parameters whose value was replaced with the sentinel
if (emptyHandling === 'omit') {
let cleaned = joined;
// Remove segments like "&Category=\u0000OMIT" or "?Category=\u0000OMIT"
// Track whether anything was actually removed so we don't corrupt non-URL strings
const beforeRemoval = cleaned;
cleaned = cleaned.replace(/[?&][^?&=#]+=\u0000OMIT/g, '');
const anythingRemoved = cleaned !== beforeRemoval;
// Only apply URL structural fixes if we actually stripped something.
// This prevents accidentally trimming a trailing '?' from a sentence or question string.
if (anythingRemoved) {
// Fix "?&" left behind when the first param was stripped (e.g. ?&Year=2024 → ?Year=2024)
cleaned = cleaned.replace(/\?&/g, '?');
// Remove a trailing "?" or "&" only if all params were stripped and nothing follows it
cleaned = cleaned.replace(/[?&]$/, '');
}
return cleaned;
}
return joined;
}
//# sourceMappingURL=handleBarsSub.js.map