UNPKG

@mikezimm/fps-core-v7

Version:

Library of reusable core interfaces, types and constants migrated from fps-library-v2

154 lines 8.54 kB
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