-- import addon read namespace from global env local _G = _G local Grichelde = _G.Grichelde local nilOrEmpty, ipairs, spairs, tContains, tFilter, tInsert, tConcat, find, sub, isUpper, isLower, toUpper, toLower, trim, length = Grichelde.functions.nilOrEmpty, Grichelde.functions.ipairs, Grichelde.functions.spairs, Grichelde.functions.tContains, Grichelde.functions.tFilter, Grichelde.functions.tInsert, Grichelde.functions.tConcat, Grichelde.functions.find, Grichelde.functions.sub, Grichelde.functions.isUpper, Grichelde.functions.isLower, Grichelde.functions.toUpper, 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) -- 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) -- skip if not enabled if (not self.db.profile.enabled) then self:DebugPrint("CheckReplacement : disabled") return false end -- skip if no text if nilOrEmpty(text) then return false end -- skip if wrong channel local allowedChannels = tFilter(self.db.profile.channels, function(_,v) return v == true end, function(k,_) return k end ) self:DebugPrint("CheckReplacement : allowed channels:", tConcat(allowedChannels, ", ")) local type = self:ConvertBlizChannelToType(channel) if (type == nil or not tContains(allowedChannels, type)) then self:DebugPrint("CheckReplacement : skip channel type:", 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 --- 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 lookAheads = {'|', '{', '%'} 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:", 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:TracePrint("current/char : %s,%s", current, currentChar) if ( not tContains(lookAheads, 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 \"%s\"", text) self:DebugPrint("ReplaceText : with \"%s\"", finalText) return finalText end --- Checks if the text starts with a preversable ignore pattern, such as itemLinks, textures, raid target icons or --- %-substitutons 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:", 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 "|K.-|k", -- Battle.net "|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 "%%n", -- player's name "%%z", -- player's currnt zone "%%sz", -- player's current sub-zone "%%loc", -- player's map coordinates "%%t", -- name of target "%%f", -- name of focus target "%%m", -- name of mouseover unit "%%p", -- name of player pet "%%tt" -- name of player's target's target } -- 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 -- @param text string -- @return string function Grichelde:ReplaceCharacters(text) local replacements = self.db.profile.replacements or {} self:DebugPrint("ReplaceCharacters : replacements") self:DebugPrint(replacements) local result = text local consolidate = {} -- replacements are done first for replName, replTable in spairs(replacements) do local before = result local search = replTable.searchText if not nilOrEmpty(search) then local replace = replTable.replaceText consolidate[replName] = {} if replTable.exactCase then -- exact case self:DebugPrint("ReplaceCharacters : \"%s => %s\" (exact case)", search, replace) local pos, offset = 1, 0 local oldResult = result local pos1, pos2 = find(oldResult, search, pos) while (pos1 and pos2) do self:TracePrint("pos1: %d, pos2: %d", pos1, pos2) local pre = sub(result, 1, pos1 - 1 + offset) local post = sub(result, pos2 + 1 + offset) self:TracePrint("pre: %s, post: %s", pre, post) -- actual replacement result = pre .. replace .. post self:DebugPrint("result: %s", result) -- remember positions for consolidate if replTable.consolidate then tInsert(consolidate[replName], pos1 + offset) end -- replacement text can lengthen or shorten the resulting text -- after replacement result and lowerResult can have different sizes offset = offset + length(replace) - length(search) -- update values for next iteration pos = pos2 + 1 pos1, pos2 = find(oldResult, search, pos) end else self:DebugPrint("ReplaceCharacters : \"%s => %s\" (ignoreCase)", search, replace) local pos, offset = 1, 0 local lowerResult = toLower(result) local lowerSearch = toLower(search) local pos1, pos2 = find(lowerResult, lowerSearch, pos) while (pos1 and pos2) do self:TracePrint("pos1: %d, pos2: %d", pos1, pos2) local pre = sub(result, 1, pos1 - 1 + offset) local match = sub(result, pos1 + offset, pos2 + offset) local post = sub(result, pos2 + 1 + offset) self:TracePrint("pre: %s, match: %s, post: %s", pre, match, post) -- keep cases local repl = "" local lastCase = nil for p = pos1, pos2 do self:TracePrint("p: %d", p) local c = sub(match, p - pos1 + 1, p - pos1 + 1) local r = sub(replace, p - pos1 + 1, p - pos1 + 1) or "" if (isUpper(c)) then -- all UPPER-CASE letter lastCase = true repl = repl .. toUpper(r) elseif (isLower(match)) then -- all lower_case letter lastCase = false repl = repl .. toLower(r) else -- no letter lastCase = nil repl = repl .. r end self:TracePrint("character: %s, %s", c, r) end self:TracePrint("length %d > %d", length(replace), pos2 - pos1 + 1) if (length(replace) > pos2 - pos1 + 1) then local remainingReplace = sub(replace, pos2 - pos1 + 2) local nextLetter = sub(post, 1, 1) self:TracePrint("rest: %s, n: %s, lastCase: %s", remainingReplace, nextLetter, lastCase) if (isUpper(nextLetter)) then if lastCase == nil or lastCase == false then repl = repl .. remainingReplace else repl = repl .. toUpper(remainingReplace) end elseif (isLower(nextLetter)) then if lastCase == nil or lastCase == true then repl = repl .. remainingReplace else repl = repl .. toLower(remainingReplace) end else -- no letter if lastCase == nil then repl = repl .. remainingReplace elseif lastCase == false then repl = repl .. toLower(remainingReplace) else repl = repl .. toUpper(remainingReplace) end end end -- actual replacement result = pre .. repl .. post self:DebugPrint("result: %s", result) -- remember positions for consolidate if replTable.consolidate then tInsert(consolidate[replName], pos1 + offset) end -- replacement text can be longer or shorter the resulting text -- after replacement result and lowerResult can have different sizes offset = offset + length(repl) - length(lowerSearch) -- update values for next iteration pos = pos2 + 1 pos1, pos2 = find(lowerResult, lowerSearch, pos) end end if before ~= result then self:DebugPrint("ReplaceCharacters : replaced \"%s\" with \"%s\"", before, result) end else self:DebugPrint("ReplaceCharacters : Skip replacement for empty mapping") end end -- consolidation is done last for replName, replTable in spairs(replacements) do local before = result --local search = replTable.searchText local search = replTable.searchText if not nilOrEmpty(search) then local replace = replTable.replaceText local lowerResult = toLower(result) local offset = 0 if replTable.consolidate then self:DebugPrint("ReplaceCharacters : consolidating \"%s => %s\"", search, replace) self:DebugPrint("consolidate[" .. replName .. "] is:") self:DebugPrint(consolidate[replName]) for _, pos1 in spairs(consolidate[replName]) do local pos2 = pos1 + length(replace) - 1 self:TracePrint("pos1: %d, pos2: %d", pos1, pos2) local match = sub(lowerResult, pos1, pos2) local next = sub(lowerResult, pos2 + 1, pos2 + 1 + pos2 - pos1) self:TracePrint("match: %s, next: %s", match, next) local _, p2 = find(next, "^" .. match) self:TracePrint("p2: %d", p2) if (p2) then result = sub(result, 1, pos2 + offset) .. sub(result, pos2 + 1 + p2 + offset) -- consolidation will shorten the resulting text offset = offset + length(result) - length(lowerResult) end self:DebugPrint("result: %s", result) end end if before ~= result then self:DebugPrint("ReplaceCharacters : consolidate \"%s\" with \"%s\"", before, result) end else self:DebugPrint("ReplaceCharacters : Skip consolidation for empty mapping") end end self:DebugPrint("ReplaceCharacters : final text:", result) return result 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 or "") 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