-- import addon read namespace from global env local _G = _G local Grichelde = _G.Grichelde local ipairs, tContains, tFilter, tInsert, tConcat, find, sub, gsub, match, toLower, trim, length = Grichelde.functions.ipairs, Grichelde.functions.tContains, Grichelde.functions.tFilter, Grichelde.functions.tInsert, Grichelde.functions.tConcat, Grichelde.functions.find, Grichelde.functions.sub, Grichelde.functions.gsub, Grichelde.functions.match, Grichelde.functions.toLower, Grichelde.functions.trim, Grichelde.functions.length --- Before a chat message is sent, check if replacement is required and replace the text accordingly. --- @param message string --- @param type string --- @param language string --- @param channel string function Grichelde:SendChatMessage(message, type, language, channel, ...) local replacedText = self:CheckAndReplace(message, type) self:DebugPrint("SendChatMessage : replacedText: " .. replacedText) -- Send text in chunks if length exceeds 255 bytes after replacement local chunks = self:SplitText(replacedText) self:DebugPrint("SendChatMessage : #chunks: " .. #chunks) for _, chunk in ipairs(chunks) do self.hooks["SendChatMessage"](chunk, type, language, channel, ...); end end function Grichelde:CheckAndReplace(message, type) local text = message if (self:CheckReplacement(text, type)) then if (_G.Misspelled) then self:DebugPrint("Misspelled detected: cleansing message") text = _G.Misspelled:RemoveHighlighting(text) end text = self:ReplaceText(trim(text)) end return text end function Grichelde:CheckReplacement(text, channel) if (not self.db.profile.enabled) then self:DebugPrint("CheckReplacement : disabled") return false end -- check channel type local allowedChannels = tFilter(self.db.profile.channels, function(_,v) return v == true end, function(k,_) return k end ) self:DebugPrint("CheckReplacement : allowed channels: %s", tConcat(allowedChannels, ", ")) local type = self:ConvertBlizChannelToType(channel) if (type ~= nil and not tContains(allowedChannels, type)) then self:DebugPrint("CheckReplacement : skip channel type %s", type) return false end -- don't replace slash commands except chat related commands if sub(text, 1, 1) == "/" then local firstWord, _ = self:SplitOnFirstMatch(text) -- todo: adapt allowed slash commands if (firstWord == nil or not tContains(self.slashCommands, firstWord)) then self:DebugPrint("CheckReplacement : ignore slash command") return false end end -- in any other case return true end function Grichelde:ConvertBlizChannelToType(channel) local type = toLower(channel) self:DebugPrint("ConvertBlizChannelToType : convert %s to %s", channel, type) return type end --- Checks if the text starts with a preversable ignore pattern, such as itemLinks, textures or raid target icons --- and returns the end location of the match, or 0 if no pattern was found --- @param text string --- @return number function Grichelde:CheckForPreversableText(text) self:DebugPrint("CheckForPreversableText : text is " .. text) -- do not replace these patterns local ignorePatterns = { "|[Cc]%x%x%x%x%x%x%x%x.-|r", -- colored items (or links) "|H.-|h", -- item links (http://www.wowwiki.com/ItemLink) "|T.-|t", -- textures "|n", -- newline "{rt[1-8]}", -- rumbered raid target icons "{Star}", -- named raid target icon 1 "{Circle}", -- named raid target icon 2 "{Coin}", -- named raid target icon 2 "{Diamond}", -- named raid target icon 3 "{Triangle}", -- named raid target icon 4 "{Moon}", -- named raid target icon 5 "{Square}", -- named raid target icon 6 "{Cross}", -- named raid target icon 7 "{X}", -- named raid target icon 7 "{Skull}" -- named raid target icon 8 } -- Calling find on ever pattern might be inefficient but its way less code. for _, pattern in ipairs(ignorePatterns) do local pos1, pos2 = find(text, pattern) if pos1 == 1 and pos2 ~= nil then self:DebugPrint("CheckForPreversableText : Found ignore pattern \"%s\" at (%d, %d)", pattern, pos1, pos2) return pos2 end end self:DebugPrint("CheckForPreversableText : no ignore pattern found") return 0 end --- Replaces all character occurrences for which replacements have been defined in the options, --- while preserving any itemLinks or textures. (http://www.wowwiki.com/ItemLink) --- @param text string --- @return string function Grichelde:ReplaceText(text) local finalText = "" local newText = text -- don't replace non-chat related slash commands local firstWord, line = self:SplitOnFirstMatch(text) if (firstWord ~= nil and tContains(self.slashCommands, firstWord)) then self:DebugPrint("ReplaceText : Found slash command %s", firstWord ) -- skip chat slash command finalText = finalText .. firstWord .. ' ' newText = line end local current = 1 local lastStart = 1 while current <= length(newText) do local currentChar = sub(newText, current, current) self:DebugPrint("current/char : %s,%s", current, currentChar) if currentChar ~= '|' and currentChar ~= '{' then current = current + 1 else -- lookahead-check for itemLinks, textures and raid target icons local textAhead = sub(newText, current) local posEnd = self:CheckForPreversableText(textAhead) if posEnd > 0 then self:DebugPrint("ReplaceText : Found an ignore pattern") local textBehind = sub(newText, lastStart, current - 1) local replacement = self:ReplaceCharacters(textBehind) local preservedText = sub(textAhead, 1, posEnd) finalText = finalText .. replacement .. preservedText current = current + posEnd lastStart = current self:DebugPrint("ReplaceText : restarting at " .. lastStart) else -- no corresponding end was found to start pattern, continue loop with next char current = current + 1 end end end -- cleanup remaining text to the end local remainingText = sub(newText, lastStart) local replacement = self:ReplaceCharacters(remainingText) finalText = finalText .. replacement self:DebugPrint("ReplaceText : replaced \"" .. text .. "\"") self:DebugPrint("ReplaceText : with \"" .. finalText .. "\"") return finalText end --- Replaces all character occurrences for which replacements have been defined in the options --- @param text string --- @return string function Grichelde:ReplaceCharacters(text) -- todo: read from options -- todo: case (in)sensitivity -- todo: consolidate consecutive -- todo: prevent infinite loops - is that even possible? local replacement = text replacement = gsub(replacement, "s", "ch") replacement = gsub(replacement, "S", "Ch") replacement = gsub(replacement, "t", "k") replacement = gsub(replacement, "T", "K") self:DebugPrint("ReplaceCharacters : replaced \"%s\" with \"%s\"", text, replacement) return replacement end --- Splits a long text in longest possible chunks of <= 255 length, split at last available space --- @param text string --- @return table function Grichelde:SplitText(text) local chunks = {} local splitText = text local textSize = length(splitText) while textSize > 255 do local chunk = sub(splitText, 1, 255) local remaining = "" -- special case: if space is the start of the next chunk, don't split this chunk if sub(splitText, 256, 256) ~= ' ' then -- split at last space, don't assign directly as nil might be returned local left, right = self:SplitOnLastMatch(chunk) if left ~= nil then chunk = left end if right ~= nil then remaining = right end end self:DebugPrint("SplitText : chunk: " .. chunk ) tInsert(chunks, chunk) splitText = remaining .. sub(splitText, 256) textSize = length(splitText) end -- pickup remaining text < 255 self:DebugPrint("SplitText : last chunk: " .. splitText) tInsert(chunks, splitText) return chunks end -- split at first word of a text line function Grichelde:SplitOnFirstMatch(text, start) self:DebugPrint("SplitOnFirstMatch : text: %s, start: %d", text, start) local pos = start or 1 local left, right = match(text, "^.- .+", pos) self:DebugPrint("SplitOnFirstMatch : left: %s, right: %s", left, right) return left or text, right end -- split at last word of a text line function Grichelde:SplitOnLastMatch(text, start) self:DebugPrint("SplitOnLastMatch : text: %s, start: %d", text, start) local pos = start or 1 local left, right = match(text, ".+ .-$", pos) self:DebugPrint("SplitOnLastMatch : left: %s, right: %s", left, right) return left, right or text end