DigitalConsumer.FixMyGlass/src/helpers/cms-content-helper.js
2026-02-27 10:15:23 -05:00

356 lines
13 KiB
JavaScript

import { storeActions } from "@/constants/store-actions.js";
import store from "@/store";
import { dynamicStrings } from "@/constants/dynamic-strings";
export function fetchCmsContentForPage(fmgPage) {
return store.dispatch(storeActions.GET_PAGE_DATA, { pageName: fmgPage }).then((response) => {
const pageDataFromCms = {};
response.data.Result.forEach((widget) => {
let widgetWithReplacements = findAndReplaceGlobalStateValues(widget.Model, widget.Name);
// If we already have this widget, push it on the collection
if (widgetWithReplacements.Name in pageDataFromCms) {
pageDataFromCms[widgetWithReplacements.Name].push(widgetWithReplacements.Model);
return;
}
pageDataFromCms[widgetWithReplacements.Name] = [widgetWithReplacements.Model];
});
Object.keys(pageDataFromCms).forEach((key) => {
if (pageDataFromCms[key].length === 1) {
pageDataFromCms[key] = pageDataFromCms[key][0];
}
});
return pageDataFromCms;
});
}
// Function to convert a string, into a matching global state item.
function mapStringToState(str) {
// Pull all matches out of the string.
const regexExp = new RegExp("{([^{}]*?):([^{}]*?)}", "g");
const regexMatches = [...str.matchAll(regexExp)];
const globalStateMatches = regexMatches.filter((match) => {
return match[1] === dynamicStrings.GLOBAL_STATE;
});
// Our final string value that will be built from the matches.
let stringBuilder = "";
for (const match of globalStateMatches) {
const valueFromStore = getStoreValueFromString(match[2]);
if (!valueFromStore) {
return ""; // if we can't map our string to state data, return an empty string.
}
const stringWithReplacement = str.replace(match[0], valueFromStore);
// If we still have values we need to substitute, call this function again.
if (stringWithReplacement.includes(dynamicStrings.GLOBAL_STATE)) {
return mapStringToState(stringWithReplacement);
}
// Concatenate the string.
stringBuilder = `${stringBuilder} ${stringWithReplacement}`;
}
return stringBuilder.trimStart();
}
// Parent function for processWidgetItemForReplacement. This will loop through the parent
// object and pass any objects that need additional processing to the processWidgetItemForReplacement function.
function findAndReplaceGlobalStateValues(widgetModel, widgetName) {
const objWithReplacements = {
Name: widgetName,
Model: {},
};
Object.keys(widgetModel).forEach((key) => {
let modelWithReplacements = processWidgetItemForReplacement(widgetModel, key);
objWithReplacements.Model[key] = modelWithReplacements;
});
return objWithReplacements;
}
// This function will process the widget item and replace any global state variables with their values.
// This is a recursive function, it will call itself until it runs out of items to iterate on given the object.
function processWidgetItemForReplacement(widgetModel, key) {
// If we have a string, and it needs to be replaced.
if (typeof widgetModel[key] === "string") {
widgetModel[key] = processIfStatements(
widgetModel[key],
dynamicStrings.GLOBAL_STATE,
getStoreValueFromString
);
if (widgetModel[key].includes(dynamicStrings.GLOBAL_STATE)) {
widgetModel[key] = mapStringToState(widgetModel[key]);
}
return widgetModel[key];
}
// If we have an object. array, etc
if (typeof widgetModel[key] === "object" && Object.keys(widgetModel[key]).length) {
Object.keys(widgetModel[key]).forEach((item) => {
processWidgetItemForReplacement(widgetModel[key], item);
});
return widgetModel[key];
}
// If we have something else like a number, boolean, etc. just return it
return widgetModel[key];
}
function getStoreValueFromString(str) {
let storeOrStateObject = str.includes("getters") ? store : store.state;
for (const s of str.split(".")) {
if (storeOrStateObject[s] != undefined) {
storeOrStateObject = storeOrStateObject[s];
} else {
return ""; // if we can't map our string to state data, return an empty string.
}
}
return storeOrStateObject;
}
///////////////////////////////////
// If Statement Processing Logic //
///////////////////////////////////
/**
* Recursive function - replaces all instances of if statements from the CMS that utilize the specified ifConditionKeyword
* @param {*} str string - Input string to be processed
* @param {*} ifConditionKeyword string - Defines which if statements to process ex: 'globalState'
* @param {*} replacePlaceholderCallback function - Callback to replace CMS placeholder values
* @returns The processed string
*/
export function processIfStatements(str, ifConditionKeyword, replacePlaceholderCallback) {
const containsRelevantIfStatement = new RegExp("{if:" + ifConditionKeyword + ":.+?}", "g").test(
str
);
if (!containsRelevantIfStatement) {
return str;
} else {
const ifStatementRegexExpression = getIfStatementRegexExpression();
const ifStatementRegexMatches = [...str.matchAll(ifStatementRegexExpression)];
const completeIfStatementArray = getAndFlagFirstNonNestedIfStatementWithKeyword(
ifStatementRegexMatches,
ifConditionKeyword
);
executeIfStatementAndSetProcessedStrings(
completeIfStatementArray,
replacePlaceholderCallback
);
const reconstructedPostProcessedString = joinProcessedRegexArray(ifStatementRegexMatches);
return processIfStatements(
reconstructedPostProcessedString,
ifConditionKeyword,
replacePlaceholderCallback
);
}
}
function getAndFlagFirstNonNestedIfStatementWithKeyword(matches, ifConditionKeyword) {
let index = 0;
for (const match of matches) {
if (match.groups.isIfStatement && match.groups.ifConditionType === ifConditionKeyword) {
let interiorIndex = 0;
let nestedLevel = 0;
let elseStatementIndex = null;
for (const interiorMatch of matches.slice(index + 1)) {
if (interiorMatch.groups.isIfStatement) {
if (interiorMatch.groups.ifConditionType === ifConditionKeyword) {
break;
} else {
nestedLevel++;
}
} else if (interiorMatch.groups.isElseStatement) {
if (!nestedLevel) {
elseStatementIndex = interiorIndex + 1;
}
} else if (interiorMatch.groups.isEndStatement) {
if (nestedLevel) {
nestedLevel--;
} else {
const ifStatementArray = matches.slice(index, index + interiorIndex + 2);
flagMatchesForProcessing(ifStatementArray, elseStatementIndex);
return ifStatementArray;
}
}
interiorIndex++;
}
}
index++;
}
}
function flagMatchesForProcessing(matches, elseStatementIndex) {
matches[0].isFlaggedForProcessing = true;
matches[matches.length - 1].isFlaggedForProcessing = true;
if (elseStatementIndex) {
matches[elseStatementIndex].isFlaggedForProcessing = true;
}
}
function joinProcessedRegexArray(regexMatches) {
let processedString = "";
regexMatches.forEach((match) => {
const rawString = match[0];
processedString += match.groups.processedString ?? rawString;
});
return processedString;
}
function executeIfStatementAndSetProcessedStrings(ifStatementArray, replacePlaceholderCallback) {
const ifCondition = replacePlaceholderCallback(ifStatementArray[0].groups.ifCondition);
let isInsideDesiredBlock = ifCondition;
ifStatementArray.forEach((entry) => {
if (entry.groups.isElseStatement && entry.isFlaggedForProcessing) {
isInsideDesiredBlock = !ifCondition;
} else if (entry.groups.isEndStatement && entry.isFlaggedForProcessing) {
isInsideDesiredBlock = true;
}
setProcessedStringOnEntry(entry, isInsideDesiredBlock);
});
}
function setProcessedStringOnEntry(entry, isInsideDesiredBlock) {
if (!isInsideDesiredBlock) {
entry.groups.processedString = "";
} else {
if (entry.groups.isIfStatement) {
entry.groups.processedString = entry.isFlaggedForProcessing
? entry.groups.ifTrailingString
: entry[0];
} else if (entry.groups.isElseStatement) {
entry.groups.processedString = entry.isFlaggedForProcessing
? entry.groups.elseTrailingString
: entry[0];
} else {
entry.groups.processedString = entry.isFlaggedForProcessing
? entry.groups.endTrailingString
: entry[0];
}
}
}
function getIfStatementRegexExpression() {
// Matches but does not capture:
// {if:...} or {else} or {end}
const anyLogicOperatorNonCapture = "(?:{(?:end|else|if:.*?)})";
// NOTE: ?<variableName> syntax stores the captured match like so: match.groups.variableName
const matchStartOfString =
"(?<processedString>^.+?)" + // Match and Capture all characters (lazy), cannot be empty
"(?=(?:{if))"; // Looks ahead but does not capture {if
const matchIfOperator =
"(?<isIfStatement>{if:)" + // Match & Capture {if:
"(?<ifConditionType>.*?):" + // Match all chars up to and including next ":" - Capture all chars up to ":"
"(?<ifCondition>.*?)}" + // Match all chars up to and including next "}" - Capture all chars up to "}"
"(?<ifTrailingString>.*?)" + // Match and Capture all characters (lazy), can be empty
"(?=" +
anyLogicOperatorNonCapture +
")"; // Looks ahead but does not capture the next logic operator
const matchElseOperator =
"(?<isElseStatement>{else})" + // Match & Capture {else}
"(?<elseTrailingString>.*?)" + // Match & Capture all characters (lazy), can be empty
"(?=" +
anyLogicOperatorNonCapture +
")"; // Looks ahead but does not capture the next logic operator
const matchEndOperator =
"(?<isEndStatement>{end})" + // Match & Capture {end}
"(?<endTrailingString>.*?)" + // Match & Capture all chracters (lazy), can be empty
"(?=" +
anyLogicOperatorNonCapture +
"|$)"; // Looks ahead but does not capture the next logic operator
// Combine all matching patterns, separated by "or" pipes
return new RegExp(
matchStartOfString +
"|" +
matchIfOperator +
"|" +
matchElseOperator +
"|" +
matchEndOperator,
"g"
);
}
//////////////////////////////////////////
// End of If Statement Processing Logic //
//////////////////////////////////////////
export function doesCopyContainRouterLink(copy) {
return copy.includes(dynamicStrings.ROUTER_LINK);
}
export function doesCopyContainTextLink(copy) {
return copy.includes(dynamicStrings.TEXT_LINK);
}
/**
* splits copy on { ... } such as {routerlink: ...}
* @returns array of strings
*/
export function splitCopyOnCMSPlaceHolder(copy) {
return copy.split(/{(.*?)}/g).filter((str) => str.length > 0);
}
/**
* Returns string2 of input following this pattern: {string1:string2,string3}
* @returns string
*/
export function getRouterLinkRouteFromCopy(copy) {
// sample input: {routerLink:estimate,provide your VIN}
// first split would return 'estimate,provide your VIN'
// second split would return 'estimate'
return copy.split(":")[1].split(",")[0];
}
export function getExternalLink(copy) {
let url = copy.split(":")[1].split(",")[0];
if (url.includes("https-")) {
const prefixAdded = url.replace("https-", "https://");
return prefixAdded;
}
return "#!";
}
/**
* Returns string3 of input following this pattern: {string1:string2,string3}
* @returns string
*/
export function getRouterLinkDisplayTextFromCopy(copy) {
// sample input: {routerLink:estimate,provide your VIN}
// first split would return 'estimate,provide your VIN'
// second split would return 'provide your VIN'
return copy.split(":")[1].split(",")[1];
}
// Copy returned from the CMS that has newlines will return blocks wrapped in
// <p ... >...</p>
// This function returns an array of each paragraph, works with or without html
// attributes present
export function splitCMSCopyOnParagraphTag(copy) {
// filter removes empty strings that are a result of string.split with regex
return copy.split(/(?:<p(?:.*?)>)|(?:<\/p>)/g).filter((paragraph) => paragraph !== "");
}
export function splitCMSCopyOnBR(copy) {
return copy.split("<br>");
}
// This function takes in a number and returns it formatted as USD currency, with or without cents depending on if the number is an integer or not.
export function formatToUSDollar(amount) {
const isInteger = amount % 1 === 0;
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: isInteger ? 0 : 2,
maximumFractionDigits: 2,
}).format(amount);
}