|
|
|
-- import addon read namespace from global env
|
|
|
|
local _G = _G
|
|
|
|
local Grichelde = _G.Grichelde
|
|
|
|
|
|
|
|
local IsAddOnLoaded, nilOrEmpty, ipairs, spairs, tContains, tFilter, tInsert, tConcat, find, sub, isUpper, isLower, toUpper, toLower, trim, length
|
|
|
|
= Grichelde.functions.IsAddOnLoaded, 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 (IsAddOnLoaded("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 and pos1 <= 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 and pos1 <= 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
|