diff --git a/tgui/packages/tgui-panel/chat/renderer.tsx b/tgui/packages/tgui-panel/chat/renderer.tsx index 0b2a82554f6..b1758b5fb5b 100644 --- a/tgui/packages/tgui-panel/chat/renderer.tsx +++ b/tgui/packages/tgui-panel/chat/renderer.tsx @@ -176,6 +176,7 @@ class ChatRenderer { highlightWholeMessage: boolean; highlightBlacklist: boolean; blacklistregex: RegExp; + enabled: boolean; }[] | null; databaseBackendEnabled: boolean; @@ -288,6 +289,7 @@ class ChatRenderer { const highlightWholeMessage = setting.highlightWholeMessage; const matchWord = setting.matchWord; const matchCase = setting.matchCase; + const enabled = setting.enabled; const allowedRegex = /^[a-zа-яё0-9_\-$/^[\s\]\\]+$/gi; const regexEscapeCharacters = /[!#$%^&*)(+=.<>{}[\]:;'"|~`_\-\\/]/g; // Reset lastIndex so it does not mess up the next word @@ -397,6 +399,7 @@ class ChatRenderer { this.highlightParsers = []; } this.highlightParsers.push({ + enabled, highlightWords, highlightRegex, highlightColor, @@ -640,31 +643,32 @@ class ChatRenderer { // Highlight text if (!message.avoidHighlighting && this.highlightParsers) { - this.highlightParsers.map((parser) => { - const ourUser = node.getElementsByClassName('name'); - const isEmote = node.getElementsByClassName('emote'); - if ( - !( - parser.highlightBlacklist && - parser.blacklistregex && - ((ourUser.length > 0 && - parser.blacklistregex.test(ourUser[0].textContent)) || - (isEmote.length > 0 && - parser.blacklistregex.test(isEmote[0].textContent))) - ) - ) { - const highlighted = highlightNode( - node, - parser.highlightRegex, - parser.highlightWords, - (text) => createHighlightNode(text, parser.highlightColor), - ); - if (highlighted && parser.highlightWholeMessage) { - node.className += ' ChatMessage--highlighted'; + this.highlightParsers + .filter((parser) => parser.enabled) + .forEach((parser) => { + const ourUser = node.getElementsByClassName('name'); + const isEmote = node.getElementsByClassName('emote'); + if ( + !( + parser.highlightBlacklist && + parser.blacklistregex && + ((ourUser.length > 0 && + parser.blacklistregex.test(ourUser[0].textContent)) || + (isEmote.length > 0 && + parser.blacklistregex.test(isEmote[0].textContent))) + ) + ) { + const highlighted = highlightNode( + node, + parser.highlightRegex, + parser.highlightWords, + (text) => createHighlightNode(text, parser.highlightColor), + ); + if (highlighted && parser.highlightWholeMessage) { + node.className += ' ChatMessage--highlighted'; + } } - } - return undefined; - }); + }); } // Linkify text const linkifyNodes = node.querySelectorAll('.linkify'); diff --git a/tgui/packages/tgui-panel/settings/SettingTabs/TextHighlightSettings.tsx b/tgui/packages/tgui-panel/settings/SettingTabs/TextHighlightSettings.tsx index f62ea6f18d5..27c48267958 100644 --- a/tgui/packages/tgui-panel/settings/SettingTabs/TextHighlightSettings.tsx +++ b/tgui/packages/tgui-panel/settings/SettingTabs/TextHighlightSettings.tsx @@ -1,4 +1,5 @@ import { useAtomValue } from 'jotai'; +import { useMemo } from 'react'; import { Box, Button, @@ -14,7 +15,7 @@ import { settingsAtom } from '../atoms'; import { MAX_HIGHLIGHT_SETTINGS } from '../constants'; import { useHighlights } from '../use-highlights'; -export const TextHighlightSettings = (props) => { +export function TextHighlightSettings(props) { const { highlights: { highlightSettings }, addHighlight, @@ -59,9 +60,25 @@ export const TextHighlightSettings = (props) => { ); -}; +} -const TextHighlightSetting = (props) => { +const oneCharacterRegex = /^(\[.*\]|\\.|.)$/; + +function extractRegex(highlight: string): string | null { + if ( + highlight.charAt(0) !== '/' || + highlight.charAt(highlight.length - 1) !== '/' + ) { + return null; + } + const expr = highlight.substring(1, highlight.length - 1); + if (oneCharacterRegex.test(expr)) { + return null; + } + return expr; +} + +function TextHighlightSetting(props) { const { id, ...rest } = props; const { highlights: { highlightSettingById }, @@ -69,6 +86,7 @@ const TextHighlightSetting = (props) => { removeHighlight, } = useHighlights(); const { + enabled, highlightColor, highlightText, blacklistText, @@ -77,6 +95,22 @@ const TextHighlightSetting = (props) => { matchWord, matchCase, } = highlightSettingById[id]; + + const highlightRegex = useMemo( + () => extractRegex(highlightText), + [highlightText], + ); + + const isRegexValid = useMemo(() => { + if (!highlightRegex) return true; + try { + new RegExp(highlightRegex, 'g'); + return true; + } catch { + return false; + } + }, [highlightRegex]); + return ( @@ -104,11 +138,26 @@ const TextHighlightSetting = (props) => { )} + + + updateHighlight({ + id, + enabled: !enabled, + }) + } + > + Enabled + + updateHighlight({ @@ -185,6 +234,7 @@ const TextHighlightSetting = (props) => { height="3em" value={highlightText} placeholder="Put words to highlight here. Separate terms with commas, i.e. (term1, term2, term3)" + style={{ border: isRegexValid ? '' : '1px solid red' }} onBlur={(value) => updateHighlight({ id, @@ -208,4 +258,4 @@ const TextHighlightSetting = (props) => { )} ); -}; +} diff --git a/tgui/packages/tgui-panel/settings/atoms.ts b/tgui/packages/tgui-panel/settings/atoms.ts index c1dca966b96..9e0f3e86b4e 100644 --- a/tgui/packages/tgui-panel/settings/atoms.ts +++ b/tgui/packages/tgui-panel/settings/atoms.ts @@ -46,6 +46,7 @@ export const defaultHighlightSetting: HighlightSetting = { highlightWholeMessage: true, matchWord: false, matchCase: false, + enabled: true, }; export const defaultHighlights: HighlightState = { diff --git a/tgui/packages/tgui-panel/settings/migration.ts b/tgui/packages/tgui-panel/settings/migration.ts index 0340a7e1621..abc18c54772 100644 --- a/tgui/packages/tgui-panel/settings/migration.ts +++ b/tgui/packages/tgui-panel/settings/migration.ts @@ -60,6 +60,17 @@ function migrateHighlights(next: HighlightState): HighlightState { draft.highlightText ?? defaultHighlightSetting.highlightText; } + // Ensure that all highlights have the "enabled" var, + // setting it to true if it doesn't exist. + for (const id in draft.highlightSettingById) { + if ( + draft.highlightSettingById[id] && + draft.highlightSettingById[id].enabled === undefined + ) { + draft.highlightSettingById[id].enabled = true; + } + } + return draft; } diff --git a/tgui/packages/tgui-panel/settings/types.ts b/tgui/packages/tgui-panel/settings/types.ts index 55668843804..da1dbf2b3f9 100644 --- a/tgui/packages/tgui-panel/settings/types.ts +++ b/tgui/packages/tgui-panel/settings/types.ts @@ -47,6 +47,7 @@ export type HighlightSetting = { id: string; matchCase: boolean; matchWord: boolean; + enabled: boolean; }; export type HighlightState = { diff --git a/tgui/packages/tgui/interfaces/ParticleEdit/helpers.ts b/tgui/packages/tgui/interfaces/ParticleEdit/helpers.ts index 0728f786a8b..198b14e8dcd 100644 --- a/tgui/packages/tgui/interfaces/ParticleEdit/helpers.ts +++ b/tgui/packages/tgui/interfaces/ParticleEdit/helpers.ts @@ -40,14 +40,13 @@ export const setGradientSpace = ( space: number, ) => { let found = false; - gradient?.map((entry) => { + gradient?.forEach((entry) => { if (typeof entry === 'object') { if (Object.keys(entry)[0] === 'space') { entry.space = space; found = true; } } - return undefined; }); if (!found) { gradient.push({ space: space }); diff --git a/tgui/packages/tgui/interfaces/VorePanel/VoreSelectedBellyTabs/VoreContentsPanel.tsx b/tgui/packages/tgui/interfaces/VorePanel/VoreSelectedBellyTabs/VoreContentsPanel.tsx index 1fbaa714a41..c22c6ce583c 100644 --- a/tgui/packages/tgui/interfaces/VorePanel/VoreSelectedBellyTabs/VoreContentsPanel.tsx +++ b/tgui/packages/tgui/interfaces/VorePanel/VoreSelectedBellyTabs/VoreContentsPanel.tsx @@ -56,18 +56,8 @@ export const VoreContentsPanel = (props: { }, [contents]); function bellyValueToName(value: string) { - const bellyName = bellyDropdownNames - ?.map((entry) => { - if (entry.value === value) { - return entry.displayText; - } - return undefined; - }) - .filter((value) => value !== undefined); - if (Array.isArray(bellyName) && bellyName.length) { - return bellyName[0]; - } - return ''; + const entry = bellyDropdownNames?.find((entry) => entry.value === value); + return entry ? entry.displayText : ''; } const contentSearch = createSearch(