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: ? syntax stores the captured match like so: match.groups.variableName const matchStartOfString = "(?^.+?)" + // Match and Capture all characters (lazy), cannot be empty "(?=(?:{if))"; // Looks ahead but does not capture {if const matchIfOperator = "(?{if:)" + // Match & Capture {if: "(?.*?):" + // Match all chars up to and including next ":" - Capture all chars up to ":" "(?.*?)}" + // Match all chars up to and including next "}" - Capture all chars up to "}" "(?.*?)" + // Match and Capture all characters (lazy), can be empty "(?=" + anyLogicOperatorNonCapture + ")"; // Looks ahead but does not capture the next logic operator const matchElseOperator = "(?{else})" + // Match & Capture {else} "(?.*?)" + // Match & Capture all characters (lazy), can be empty "(?=" + anyLogicOperatorNonCapture + ")"; // Looks ahead but does not capture the next logic operator const matchEndOperator = "(?{end})" + // Match & Capture {end} "(?.*?)" + // 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); } /** * 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]; } /** * 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 //

...

// 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>)/g).filter((paragraph) => paragraph !== ""); }