--[[--------------------------------------------------------------------------- Grichelde Copyright 2020 Teilzeit-Jedi based on Misspelled developed by Nathan Pieper - nrpieper (@) gmail (dot) com This code freely distributed for your use in any GPL compliant project. See conditions in the LICENSE file attached. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --------------------------------------------------------------------------]] -- local AddonName, AddonTable = ... local Grichelde = LibStub("AceAddon-3.0"):NewAddon("Grichelde", "AceEvent-3.0", "AceHook-3.0") Grichelde.version = GetAddOnMetadata(AddonName, "Version") Grichelde.build = GetAddOnMetadata(AddonName, "X-Build") or "UNKNOWN" local L = LibStub("AceLocale-3.0"):GetLocale("Grichelde", true) local Grichelde_Debug = false -- faster function lookups by mapping to local refs local string_find = string.find local string_gsub = string.gsub local string_len = string.len local string_rep = string.rep local string_sub = string.sub local strtrim = strtrim local strmatch = strmatch local tostring = tostring local tInsert = table.insert local tContains = tContains local pairs = pairs local ipairs = ipairs local Grichelde_Hooks = {} --local Grichelde_ChatTypes = { "SAY", "EMOTE", "YELL", "PARTY", "GUILD", "OFFICER", "RAID", "RAID_WARNING", "INSTANCE_CHAT", "BATTLEGROUND", "WHISPER" } local Grichelde_ChatTypes = { "SAY", "EMOTE", "YELL", "PARTY", "GUILD" } local Grichelde_ChatCommands = { "/s", "/e", "/me", "/y", "/p", "/pl", "/g", "/o", "/raid", "/rl", "/rw", "/i", "bg", "/w", "/r", "/tt" } -- do not replace these patterns local Grichelde_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 } function Grichelde:OnInitialize() -- Build Interface Options window --self:CreateInterfaceOptions() -- Watch for WIM and Prat to Load, then integrate self:RegisterEvent("ADDON_LOADED", "HookIntoForOtherChatAddons") end function Grichelde:OnEnable() -- Hook in before message is sent to replace all character occurrences where replacements have been defined in the options self:RawHook("SendChatMessage", true) if (Misspelled) then print("Misspelled detected, Grichelde will have any messsage being cleansed") end -- tell the world we are listening print(L.AddonLoaded) end function Grichelde:OnDisable() self:Unhook("SendChatMessage") end --- @param event string --- @param addonName string function Grichelde:HookIntoForOtherChatAddons(event, addonName) if event == "ADDON_LOADED" then if addonName == "WIM" then WIM.RegisterWidgetTrigger("msg_box", "whisper,chat,w2w", "OnEnterPressed", Grichelde.EditBox_OnEnterPressed) -- If available use the WIM API if (WIM.RegisterPreSendFilterText) then -- avoid error if WIM not up to date. WIM.RegisterPreSendFilterText(function(text) return Grichelde:CheckAndReplace(text) end) else -- WIM sends its chat messages via the API ChatThrottleLib, which itself hooks the default SendChatMessage api -- many times before Grichelde will. ChatThrottleLib might potentially load before Grichelde, so we just hook -- into ChatThrottleLib to be on the safe side. if (ChatThrottleLib) then Grichelde_Hooks["ChatThrottleLib"] = ChatThrottleLib.SendChatMessage function ChatThrottleLib:SendChatMessage(prio, prefix, text, ...) Grichelde:DebugPrint("ChatThrottleLib:SendChatMessage : Hook called") local replacedText = Grichelde:CheckAndReplace(text) return Grichelde_Hooks["ChatThrottleLib"](ChatThrottleLib, prio, prefix, replacedText, ...) end end end end end end --- Before af 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 (Misspelled) then self:DebugPrint("Misspelled detected: cleansing message") text = Misspelled:RemoveHighlighting(text) end text = strtrim(text) if (self:CheckReplacement(text, type)) then text = self:ReplaceText(text) end return text end function Grichelde:CheckReplacement(text, type) -- todo: globally disabled? -- check type if (not tContains(Grichelde_ChatTypes, type)) then self:DebugPrint("CheckReplacement : skip channel type") return false end -- don't replace slash commands except chat related commands if string_sub(text, 1, 1) == "/" then local firstWord, _ = self:SplitOnFirstMatch(text) if (firstWord == nil or not tContains(Grichelde_ChatCommands, firstWord)) then self:DebugPrint("CheckReplacement : ignore slash command") return false end end -- in any other case return true 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) -- Calling find on ever pattern might be inefficient but its way less code. for _, pattern in ipairs(Grichelde_IgnorePatterns) do local pos1, pos2 = string_find(text, pattern) if pos1 == 1 and pos2 ~= nil then self:DebugPrint("CheckForPreversableText : Found ignore pattern " .. pattern .. " at (" .. 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(Grichelde_ChatCommands, firstWord)) then self:DebugPrint("ReplaceText : Found slash command " .. (firstWord + "") ) -- skip chat slash command finalText = finalText .. firstWord .. ' ' newText = line end local current = 1 local lastStart = 1 while current <= string_len(newText) do local currentChar = string_sub(newText, current, current) self:DebugPrint("current/char : " .. current .. "," .. currentChar) if currentChar ~= '|' and currentChar ~= '{' then current = current + 1 else -- lookahead-check for itemLinks, textures and raid target icons local textAhead = string_sub(newText, current) local posEnd = self:CheckForPreversableText(textAhead) if posEnd > 0 then self:DebugPrint("ReplaceText : Found an ignore pattern") local textBehind = string_sub(newText, lastStart, current - 1) local replacement = self:ReplaceCharacters(textBehind) local preservedText = string_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 = string_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 local replacement = text replacement = string_gsub(replacement, "s", "ch") replacement = string_gsub(replacement, "S", "Ch") replacement = string_gsub(replacement, "t", "k") replacement = string_gsub(replacement, "T", "K") self:DebugPrint("ReplaceCharacters : replaced \"" .. text .. "\" with \"" .. 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 = string_len(splitText) while textSize > 255 do local chunk = string_sub(splitText, 1, 255) local remaining = "" -- special case: if space is the start of the next chunk, don't split this chunk if string_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 .. string_sub(splitText, 256) textSize = string_len(splitText) end -- pickup remaining text < 255 self:DebugPrint("SplitText : last chunk: " .. splitText) tInsert(chunks, splitText) return chunks end -- split first word of a text line function Grichelde:SplitOnFirstMatch(text, start) self:DebugPrint("SplitOnFirstMatch : text: " .. text .. ", start: " .. self:EmptyIfNil(start)) local pos = 1 if start ~= nil then pos = start end local left, right = strmatch(text, "^.- .+", pos) self:DebugPrint("SplitOnFirstMatch : left: " .. self:EmptyIfNil(left) .. ", right: " .. self:EmptyIfNil(right)) return left, right end function Grichelde:SplitOnLastMatch(text, start) self:DebugPrint("SplitOnLastMatch : text: " .. text .. ", start: " .. self:EmptyIfNil(start)) local pos = 1 if start ~= nil then pos = start end local left, right = strmatch(text, ".+ .-$", pos) self:DebugPrint("SplitOnLastMatch : left: " .. self:EmptyIfNil(left) .. ", right: " .. self:EmptyIfNil(right)) return left, right end function Grichelde:DebugPrint(message) if (Grichelde_Debug) then print(GRAY_FONT_COLOR_CODE .. "Grichelde:" .. FONT_COLOR_CODE_CLOSE .. " " .. message) end end function Grichelde:EmptyIfNil(value) if value == nil then return "" end return tostring(value) end function Grichelde:tprint(t, indent, done) -- in case we run it standalone local Note = Note or print -- local Tell = Tell or io.write -- show strings differently to distinguish them from numbers local function show(val) if type(val) == "string" then return '"' .. val .. '"' else return tostring(val) end end -- entry point here done = done or {} indent = indent or 0 for key, value in pairs(t) do print(string_rep(" ", indent)) -- indent it if type(value) == "table" and not done[value] then done[value] = true Note(show(key), ":"); self:tprint(value, indent + 2, done) else print(show(key), "=") print(show(value)) end end end