diff --git a/.eslintrc.json b/.eslintrc.json index efa6c4ae0..ff6250aef 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,10 +1,13 @@ { "env": { "browser": true, - "es6": true, + "es2024": true, "jquery": true, "greasemonkey": true }, + "parserOptions": { + "sourceType": "module" + }, "extends": [ "eslint:recommended", "plugin:prettier/recommended", diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..65ecc852c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: Tests +'on': + push: + pull_request: + types: + - opened + - synchronize + - reopened + +jobs: + test: + name: 'Node.js v18' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: '18' + - name: 'Cache node_modules' + uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{ runner.os }}-node-18-${{ hashFiles('**/package.json') }} + restore-keys: | + ${{ runner.os }}-node-18- + - name: Install Dependencies + run: npm install + - name: Run All Node.js Tests + run: npm run test diff --git a/core/code/chat.js b/core/code/chat.js index 822bafe13..99e2855cb 100644 --- a/core/code/chat.js +++ b/core/code/chat.js @@ -1,11 +1,83 @@ /** * @file Namespace for chat-related functionalities. * - * @namespace chat + * @module chat */ var chat = function () {}; window.chat = chat; +// List of functions to track for synchronization between chat and comm +const legacyFunctions = [ + 'genPostData', + 'updateOldNewHash', + 'parseMsgData', + 'writeDataToHash', + 'renderText', + 'getChatPortalName', + 'renderPortal', + 'renderFactionEnt', + 'renderPlayer', + 'renderMarkupEntity', + 'renderMarkup', + 'renderTimeCell', + 'renderNickCell', + 'renderMsgCell', + 'renderMsgRow', + 'renderDivider', + 'renderData', +]; +const newCommApi = [ + '_genPostData', + '_updateOldNewHash', + 'parseMsgData', + '_writeDataToHash', + 'renderText', + 'getChatPortalName', + 'renderPortal', + 'renderFactionEnt', + 'renderPlayer', + 'renderMarkupEntity', + 'renderMarkup', + 'renderTimeCell', + 'renderNickCell', + 'renderMsgCell', + 'renderMsgRow', + 'renderDivider', + 'renderData', +]; + +// Function to map legacy function names to their new names in comm +function mapLegacyFunctionNameToCommApi(functionName) { + const index = legacyFunctions.indexOf(functionName); + return index !== -1 ? newCommApi[index] : functionName; +} + +// Create a proxy for chat to ensure backward compatibility of migrated functions from chat to comm +window.chat = new Proxy(window.chat, { + get(target, prop, receiver) { + if (prop in target) { + // Return the property from chat if it's defined + return target[prop]; + } else if (legacyFunctions.includes(prop)) { + // Map the legacy function name to its new name in comm and return the corresponding function + const commProp = mapLegacyFunctionNameToCommApi(prop); + return window.IITC.comm[commProp]; + } + // Return default value if the property is not found + return Reflect.get(target, prop, receiver); + }, + set(target, prop, value) { + if (legacyFunctions.includes(prop)) { + // Map the legacy function name to its new name in comm and synchronize the function between chat and comm + const commProp = mapLegacyFunctionNameToCommApi(prop); + window.IITC.comm[commProp] = value; + } + // Update or add the property in chat + target[prop] = value; + return true; // Indicates that the assignment was successful + }, +}); + // // common // @@ -13,7 +85,7 @@ window.chat = chat; /** * Adds a nickname to the chat input. * - * @function chat.addNickname + * @function addNickname * @param {string} nick - The nickname to add. */ chat.addNickname = function (nick) { @@ -25,7 +97,7 @@ chat.addNickname = function (nick) { /** * Handles click events on nicknames in the chat. * - * @function chat.nicknameClicked + * @function nicknameClicked * @param {Event} event - The click event. * @param {string} nickname - The clicked nickname. * @returns {boolean} Always returns false. @@ -59,31 +131,31 @@ chat.nicknameClicked = function (event, nickname) { * Hold channel description * * See comm.js for examples - * @typedef {Object} chat.ChannelDescription + * @typedef {Object} ChannelDescription * @property {string} id - uniq id, matches 'tab' parameter for server requests * @property {string} name - visible name * @property {string} [inputPrompt] - (optional) string for the input prompt * @property {string} [inputClass] - (optional) class to apply to #chatinput - * @property {chat.ChannelSendMessageFn} [sendMessage] - (optional) function to send the message - * @property {chat.ChannelRequestFn} [request] - (optional) function to call to request new message - * @property {chat.ChannelRenderFn} [render] - (optional) function to render channel content,, called on tab change + * @property {ChannelSendMessageFn} [sendMessage] - (optional) function to send the message + * @property {ChannelRequestFn} [request] - (optional) function to call to request new message + * @property {ChannelRenderFn} [render] - (optional) function to render channel content,, called on tab change * @property {boolean} [localBounds] - (optional) if true, reset on view change */ /** - * @callback chat.ChannelSendMessageFn + * @callback ChannelSendMessageFn * @param {string} id - channel id * @param {string} message - input message * @returns {void} */ /** - * @callback chat.ChannelRequestFn + * @callback ChannelRequestFn * @param {string} id - channel id * @param {boolean} getOlderMsgs - true if request data from a scroll to top * @param {boolean} isRetry * @returns {void} */ /** - * @callback chat.ChannelRenderFn + * @callback ChannelRenderFn * @param {string} id - channel id * @param {boolean} oldMsgsWereAdded - true if data has been added at the top (to preserve scroll position) * @returns {void} @@ -92,14 +164,15 @@ chat.nicknameClicked = function (event, nickname) { /** * Holds channels infos. * - * @type {chat.ChannelDescription[]} + * @type {ChannelDescription[]} + * @memberof module:chat */ chat.channels = []; /** * Gets the name of the active chat tab. * - * @function chat.getActive + * @function getActive * @returns {string} The name of the active chat tab. */ chat.getActive = function () { @@ -109,9 +182,9 @@ chat.getActive = function () { /** * Converts a chat tab name to its corresponding channel object. * - * @function chat.getChannelDesc + * @function getChannelDesc * @param {string} tab - The name of the chat tab. - * @returns {chat.ChannelDescription} The corresponding channel name ('faction', 'alerts', or 'all'). + * @returns {ChannelDescription} The corresponding channel name ('faction', 'alerts', or 'all'). */ chat.getChannelDesc = function (tab) { var channelObject = null; @@ -126,7 +199,7 @@ chat.getChannelDesc = function (tab) { * that need to process COMM data even when the user is not actively viewing the COMM channels. * It tracks the requested channels for each plugin instance and updates the global state accordingly. * - * @function chat.backgroundChannelData + * @function backgroundChannelData * @param {string} instance - A unique identifier for the plugin or instance requesting background COMM data. * @param {string} channel - The name of the COMM channel ('all', 'faction', or 'alerts'). * @param {boolean} flag - Set to true to request data for the specified channel, false to stop requesting. @@ -154,7 +227,7 @@ chat.backgroundChannelData = function (instance, channel, flag) { * Requests chat messages for the currently active chat tab and background channels. * It calls the appropriate request function based on the active tab or background channels. * - * @function chat.request + * @function request */ chat.request = function () { var channel = chat.getActive(); @@ -169,7 +242,7 @@ chat.request = function () { * Checks if the currently selected chat tab needs more messages. * This function is triggered by scroll events and loads older messages when the user scrolls to the top. * - * @function chat.needMoreMessages + * @function needMoreMessages */ chat.needMoreMessages = function () { var activeTab = chat.getActive(); @@ -190,7 +263,7 @@ chat.needMoreMessages = function () { * Chooses and activates a specified chat tab. * Also triggers an early refresh of the chat data when switching tabs. * - * @function chat.chooseTab + * @function chooseTab * @param {string} tab - The name of the chat tab to activate ('all', 'faction', or 'alerts'). */ chat.chooseTab = function (tab) { @@ -246,7 +319,7 @@ chat.chooseTab = function (tab) { * When expanded, the chat window covers a larger area of the screen. * This function also ensures that the chat is scrolled to the bottom when collapsed. * - * @function chat.toggle + * @function toggle */ chat.toggle = function () { var c = $('#chat, #chatcontrols'); @@ -266,7 +339,7 @@ chat.toggle = function () { /** * Displays the chat interface and activates a specified chat tab. * - * @function chat.show + * @function show * @param {string} name - The name of the chat tab to show and activate. */ chat.show = function (name) { @@ -285,7 +358,7 @@ chat.show = function (name) { * This function is triggered by a click event on the chat tab. It reads the tab name from the event target * and activates the corresponding chat tab. * - * @function chat.chooser + * @function chooser * @param {Event} event - The event triggered by clicking a chat tab. */ chat.chooser = function (event) { @@ -299,7 +372,7 @@ chat.chooser = function (event) { * This function is designed to keep the scroll position fixed when old messages are loaded, and to automatically scroll * to the bottom when new messages are added if the user is already at the bottom of the chat. * - * @function chat.keepScrollPosition + * @function keepScrollPosition * @param {jQuery} box - The jQuery object of the chat box. * @param {number} scrollBefore - The scroll position before new messages were added. * @param {boolean} isOldMsgs - Indicates if the added messages are older messages. @@ -327,7 +400,7 @@ chat.keepScrollPosition = function (box, scrollBefore, isOldMsgs) { * * @function createChannelTab * @memberof chat - * @param {chat.ChannelDescription} channelDesc - channel description + * @param {ChannelDescription} channelDesc - channel description * @static */ function createChannelTab(channelDesc) { @@ -362,8 +435,8 @@ var isTabsSetup = false; * * If tabs are already created, a tab is created for this channel as well * - * @function chat.addChannel - * @param {chat.ChannelDescription} channelDesc - channel description + * @function addChannel + * @param {ChannelDescription} channelDesc - channel description */ chat.addChannel = function (channelDesc) { // deny reserved name @@ -391,8 +464,8 @@ chat.addChannel = function (channelDesc) { /** * Sets up all channels starting from intel COMM * - * @function chat.setupTabs - * @param {chat.ChannelDescription} channelDesc - channel description + * @function setupTabs + * @param {ChannelDescription} channelDesc - channel description */ chat.setupTabs = function () { isTabsSetup = true; @@ -413,7 +486,7 @@ chat.setupTabs = function () { /** * Initiates a request for public chat data. * - * @function chat.requestPublic + * @function requestPublic * @param {boolean} getOlderMsgs - Whether to retrieve older messages. * @param {boolean} [isRetry=false] - Whether the request is a retry. */ @@ -424,7 +497,7 @@ chat.setupTabs = function () { /** * Requests faction chat messages. * - * @function chat.requestFaction + * @function requestFaction * @param {boolean} getOlderMsgs - Flag to determine if older messages are being requested. * @param {boolean} [isRetry=false] - Flag to indicate if this is a retry attempt. */ @@ -435,7 +508,7 @@ chat.setupTabs = function () { /** * Initiates a request for alerts chat data. * - * @function chat.requestAlerts + * @function requestAlerts * @param {boolean} getOlderMsgs - Whether to retrieve older messages. * @param {boolean} [isRetry=false] - Whether the request is a retry. */ @@ -446,7 +519,7 @@ chat.setupTabs = function () { /** * Renders public chat in the UI. * - * @function chat.renderPublic + * @function renderPublic * @param {boolean} oldMsgsWereAdded - Indicates if older messages were added to the chat. */ chat.renderPublic = function (oldMsgsWereAdded) { @@ -456,7 +529,7 @@ chat.setupTabs = function () { /** * Renders faction chat. * - * @function chat.renderFaction + * @function renderFaction * @param {boolean} oldMsgsWereAdded - Indicates if old messages were added in the current rendering. */ chat.renderFaction = function (oldMsgsWereAdded) { @@ -466,35 +539,18 @@ chat.setupTabs = function () { /** * Renders alerts chat in the UI. * - * @function chat.renderAlerts + * @function renderAlerts * @param {boolean} oldMsgsWereAdded - Indicates if older messages were added to the chat. */ chat.renderAlerts = function (oldMsgsWereAdded) { return IITC.comm.renderChannel('allerts', oldMsgsWereAdded); }; - - chat.getChatPortalName = IITC.comm.getChatPortalName; - - /** - * Renders data from the data-hash to the element defined by the given ID. - * - * @function chat.renderData - * @param {Object} data - Chat data to be rendered. - * @param {string} element - ID of the DOM element to render the chat into. - * @param {boolean} likelyWereOldMsgs - Flag indicating if older messages are likely to have been added. - * @param {Array} sortedGuids - Sorted array of GUIDs representing the order of messages. - * @memberof window.chat - * @type {Object} - */ - chat.renderData = function (data, element, likelyWereOldMsgs, sortedGuids) { - return IITC.comm.renderData(data, element, likelyWereOldMsgs, sortedGuids); - }; }; /** * Sets up the chat interface. * - * @function chat.setup + * @function setup */ chat.setup = function () { chat.setupTabs(); @@ -528,7 +584,7 @@ chat.setup = function () { * Sets up the time display in the chat input box. * This function updates the time displayed next to the chat input field every minute to reflect the current time. * - * @function chat.setupTime + * @function setupTime */ chat.setupTime = function () { var inputTime = $('#chatinput time'); @@ -554,7 +610,7 @@ chat.setupTime = function () { /** * Handles tab completion in chat input. * - * @function chat.handleTabCompletion + * @function handleTabCompletion */ chat.handleTabCompletion = function () { var el = $('#chatinput input'); @@ -595,7 +651,7 @@ chat.handleTabCompletion = function () { /** * Posts a chat message to the currently active chat tab. * - * @function chat.postMsg + * @function postMsg */ chat.postMsg = function () { var c = chat.getActive(); @@ -613,7 +669,7 @@ chat.postMsg = function () { /** * Sets up the chat message posting functionality. * - * @function chat.setupPosting + * @function setupPosting */ chat.setupPosting = function () { if (!window.isSmartphone()) { @@ -642,4 +698,58 @@ chat.setupPosting = function () { }); }; +/** + * Legacy function for rendering chat messages. Used for backward compatibility with plugins. + * + * @deprecated + * @function renderMsg + * @param {string} msg - The chat message. + * @param {string} nick - The nickname of the player who sent the message. + * @param {number} time - The timestamp of the message. + * @param {string} team - The team of the player who sent the message. + * @param {boolean} msgToPlayer - Flag indicating if the message is directed to the player. + * @param {boolean} systemNarrowcast - Flag indicating if the message is a system narrowcast. + * @returns {string} The HTML string representing a chat message row. + */ +chat.renderMsg = function (msg, nick, time, team, msgToPlayer, systemNarrowcast) { + // Imitating data usually derived from processing raw chat data + var fakeData = { + guid: 'legacyguid-' + Math.random(), + time: time, + public: !systemNarrowcast, + secure: systemNarrowcast, + alert: msgToPlayer, + msgToPlayer: msgToPlayer, + type: systemNarrowcast ? 'SYSTEM_NARROWCAST' : 'PLAYER_GENERATED', + narrowcast: systemNarrowcast, + auto: false, // Assuming the message is player-generated if it's not a system broadcast + team: team, + player: { + name: nick, + team: team, + }, + markup: [ + ['TEXT', { plain: msg }], // A simple message with no special markup + ], + }; + + // Use existing IITC functions to render a chat message row + return IITC.comm.renderMsgRow(fakeData); +}; + +/** + * Legacy function for converts a chat tab name to its corresponding COMM channel name. + * Used for backward compatibility with plugins. + * + * @deprecated + * @function tabToChannel + * @param {string} tab - The name of the chat tab. + * @returns {string} The corresponding channel name ('faction', 'alerts', or 'all'). + */ +chat.tabToChannel = function (tab) { + if (tab === 'faction') return 'faction'; + if (tab === 'alerts') return 'alerts'; + return 'all'; +}; + /* global log, PLAYER, L, IITC, app */ diff --git a/core/code/comm.js b/core/code/comm.js index f5219143c..2414e75bc 100644 --- a/core/code/comm.js +++ b/core/code/comm.js @@ -7,6 +7,7 @@ /** * @type {chat.ChannelDescription[]} + * @memberof IITC.comm */ var _channels = [ { @@ -46,6 +47,7 @@ var _channels = [ * Holds data related to each intel channel. * * @type {Object} + * @memberof IITC.comm */ var _channelsData = {}; @@ -67,6 +69,54 @@ function _initChannelData(id) { delete _channelsData[id].newestGUID; } +/** + * Template of portal link in comm. + * @type {String} + * @memberof IITC.comm + */ +let portalTemplate = + '{{ portal_name }}'; +/** + * Template for time cell. + * @type {String} + * @memberof IITC.comm + */ +let timeCellTemplate = ''; +/** + * Template for player's nickname cell. + * @type {String} + * @memberof IITC.comm + */ +let nickCellTemplate = '<{{ nick }}>'; +/** + * Template for chat message text cell. + * @type {String} + * @memberof IITC.comm + */ +let msgCellTemplate = '{{ msg }}'; +/** + * Template for message row, includes cells for time, player nickname and message text. + * @type {String} + * @memberof IITC.comm + */ +let msgRowTemplate = '{{ time_cell }}{{ nick_cell }}{{ msg_cell }}'; +/** + * Template for message divider. + * @type {String} + * @memberof IITC.comm + */ +let dividerTemplate = '
{{ text }}
'; + +/** + * Returns the coordinates for the message to be sent, default is the center of the map. + * + * @function IITC.comm.getLatLngForSendingMessage + * @returns {L.LatLng} + */ +function getLatLngForSendingMessage() { + return map.getCenter(); +} + /** * Updates the oldest and newest message timestamps and GUIDs in the chat storage. * @@ -207,7 +257,7 @@ function _writeDataToHash(newData, storageHash, isOlderMsgs, isAscendingOrder) { function sendChatMessage(tab, msg) { if (tab !== 'all' && tab !== 'faction') return; - var latlng = map.getCenter(); + const latlng = IITC.comm.getLatLngForSendingMessage(); var data = { message: msg, @@ -239,12 +289,16 @@ var _oldBBox = null; * @private * @param {string} channel - The chat channel. * @param {boolean} getOlderMsgs - Flag to determine if older messages are being requested. + * @param args=undefined - Used for backward compatibility when calling a function with three arguments. * @returns {Object} The generated post data. */ -function _genPostData(channel, getOlderMsgs) { +function _genPostData(channel, getOlderMsgs, ...args) { if (typeof channel !== 'string') { throw new Error('API changed: isFaction flag now a channel string - all, faction, alerts'); } + if (args.length === 1) { + getOlderMsgs = args[0]; + } var b = window.clampLatLngBounds(map.getBounds()); @@ -430,20 +484,46 @@ function renderText(text) { return content.html().autoLink(); } +/** + * List of transformations for portal names used in chat. + * Each transformation function takes the portal markup object and returns a transformed name. + * If a transformation does not apply, the original name is returned. + * + * @const IITC.comm.portalNameTransformations + * @example + * // Adding a transformation that appends the portal location to its name + * portalNameTransformations.push((markup) => { + * const latlng = `${markup.latE6 / 1E6},${markup.lngE6 / 1E6}`; // Convert E6 format to decimal + * return `[${latlng}] ${markup.name}`; + * }); + */ +const portalNameTransformations = [ + // Transformation for 'US Post Office' + (markup) => { + if (markup.name === 'US Post Office') { + const address = markup.address.split(','); + return 'USPS: ' + address[0]; + } + return markup.name; + }, +]; + /** * Overrides portal names used repeatedly in chat, such as 'US Post Office', with more specific names. + * Applies a series of transformations to the portal name based on the portal markup. * * @function IITC.comm.getChatPortalName * @param {Object} markup - An object containing portal markup, including the name and address. * @returns {string} The processed portal name. */ function getChatPortalName(markup) { - var name = markup.name; - if (name === 'US Post Office') { - var address = markup.address.split(','); - name = 'USPS: ' + address[0]; - } - return name; + // Use reduce to apply each transformation to the data + const transformedData = portalNameTransformations.reduce((initialMarkup, transform) => { + const updatedName = transform(initialMarkup); + return { ...initialMarkup, name: updatedName }; + }, markup); + + return transformedData.name; } /** @@ -454,11 +534,17 @@ function getChatPortalName(markup) { * @returns {string} HTML string of the portal link. */ function renderPortal(portal) { - var lat = portal.latE6 / 1e6, - lng = portal.lngE6 / 1e6; - var perma = window.makePermalink([lat, lng]); - var js = 'window.selectPortalByLatLng(' + lat + ', ' + lng + ');return false'; - return '' + IITC.comm.getChatPortalName(portal) + ''; + const lat = portal.latE6 / 1e6; + const lng = portal.lngE6 / 1e6; + const permalink = window.makePermalink([lat, lng]); + const portalName = IITC.comm.getChatPortalName(portal); + + return IITC.comm.portalTemplate + .replace('{{ lat }}', lat.toString()) + .replace('{{ lng }}', lng.toString()) + .replace('{{ title }}', portal.address) + .replace('{{ url }}', permalink) + .replace('{{ portal_name }}', portalName); } /** @@ -561,58 +647,102 @@ function renderMarkup(markup) { } /** - * Transforms a the markup array into an older, more straightforward format for easier understanding. - * - * May be used to build an entirely new markup to be rendered without altering the original one. + * List of transformations to be applied to the message data. + * Each transformation function takes the full message data object and returns the transformed markup. + * The default transformations aim to convert the message markup into an older, more straightforward format, + * facilitating easier understanding and backward compatibility with plugins expecting the older message format. * - * @function IITC.comm.transformMessage - * @param {Object} data - The data for the message, including time, player, and message content. - * @returns {Array} The transformed markup array with a simplified structure. + * @const IITC.comm.messageTransformFunctions + * @example + * // Adding a new transformation function to the array + * // This new function adds a "new" prefix to the player's plain text if the player is from the RESISTANCE team + * messageTransformFunctions.push((data) => { + * const markup = data.markup; + * if (markup.length > 2 && markup[0][0] === 'PLAYER' && markup[0][1].team === 'RESISTANCE') { + * markup[1][1].plain = 'new ' + markup[1][1].plain; + * } + * return markup; + * }); */ -function transformMessage(data) { - // Make a copy of the markup array to avoid modifying the original input - let newMarkup = JSON.parse(JSON.stringify(data.markup)); - - // Collapse + "Link"/"Field". Example: "Agent destroyed the Link ..." - if (newMarkup.length > 4) { - if (newMarkup[3][0] === 'FACTION' && newMarkup[4][0] === 'TEXT' && (newMarkup[4][1].plain === ' Link ' || newMarkup[4][1].plain === ' Control Field @')) { - newMarkup[4][1].team = newMarkup[3][1].team; - newMarkup.splice(3, 1); +const messageTransformFunctions = [ + // Collapse + "Link"/"Field". + (data) => { + const markup = data.markup; + if ( + markup.length > 4 && + markup[3][0] === 'FACTION' && + markup[4][0] === 'TEXT' && + (markup[4][1].plain === ' Link ' || markup[4][1].plain === ' Control Field @') + ) { + markup[4][1].team = markup[3][1].team; + markup.splice(3, 1); } - } - + return markup; + }, // Skip "Agent " at the beginning - if (newMarkup.length > 1) { - if (newMarkup[0][0] === 'TEXT' && newMarkup[0][1].plain === 'Agent ' && newMarkup[1][0] === 'PLAYER') { - newMarkup.splice(0, 2); + (data) => { + const markup = data.markup; + if (markup.length > 1 && markup[0][0] === 'TEXT' && markup[0][1].plain === 'Agent ' && markup[1][0] === 'PLAYER') { + markup.splice(0, 2); } - } - + return markup; + }, // Skip " agent " at the beginning - if (newMarkup.length > 2) { - if (newMarkup[0][0] === 'FACTION' && newMarkup[1][0] === 'TEXT' && newMarkup[1][1].plain === ' agent ' && newMarkup[2][0] === 'PLAYER') { - newMarkup.splice(0, 3); + (data) => { + const markup = data.markup; + if (markup.length > 2 && markup[0][0] === 'FACTION' && markup[1][0] === 'TEXT' && markup[1][1].plain === ' agent ' && markup[2][0] === 'PLAYER') { + markup.splice(0, 3); } - } + return markup; + }, +]; - return newMarkup; -} +/** + * Applies transformations to the markup array based on the transformations defined in + * the {@link IITC.comm.messageTransformFunctions} array. + * Assumes all transformations return a new markup array. + * May be used to build an entirely new markup to be rendered without altering the original one. + * + * @function IITC.comm.transformMessage + * @param {Object} data - The data for the message, including time, player, and message content. + * @returns {Object} The transformed markup array. + */ +const transformMessage = (data) => { + const initialData = JSON.parse(JSON.stringify(data)); + + // Use reduce to apply each transformation to the data + const transformedData = messageTransformFunctions.reduce((data, transform) => { + const updatedMarkup = transform(data); + return { ...data, markup: updatedMarkup }; + }, initialData); + + return transformedData.markup; +}; /** * Renders a cell in the chat table to display the time a message was sent. * Formats the time and adds it to a