|
|
|
--[[---------------------------------------------------------------------------
|
|
|
|
Grichelde
|
|
|
|
Copyright 2020 Teilzeit-Jedi <tj@teilzeit-jedi.de>
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
--------------------------------------------------------------------------]] --
|
|
|
|
-- upvalues
|
|
|
|
local _G = _G
|
|
|
|
|
|
|
|
-- faster function lookups by mapping to local refs
|
|
|
|
local string_find = _G.string.find
|
|
|
|
local string_gsub = _G.string.gsub
|
|
|
|
local string_len = _G.string.len
|
|
|
|
local string_rep = _G.string.rep
|
|
|
|
local string_sub = _G.string.sub
|
|
|
|
local string_fmt = _G.string.format
|
|
|
|
local strtrim = _G.strtrim
|
|
|
|
local strmatch = _G.strmatch
|
|
|
|
local tostring = _G.tostring
|
|
|
|
local tInsert = _G.table.insert
|
|
|
|
local tContains = _G.tContains
|
|
|
|
local unpack = _G.unpack
|
|
|
|
local pairs = _G.pairs
|
|
|
|
local ipairs = _G.ipairs
|
|
|
|
|
|
|
|
-- colors:
|
|
|
|
local PREFIX_COLOR_CODE = "|c00FFAA00"
|
|
|
|
|
|
|
|
-- initialize addon
|
|
|
|
local AddonName, AddonTable = ...
|
|
|
|
|
|
|
|
local Grichelde = LibStub("AceAddon-3.0"):NewAddon(AddonName, "AceConsole-3.0", "AceEvent-3.0", "AceHook-3.0")
|
|
|
|
Grichelde.L = LibStub("AceLocale-3.0"):GetLocale("Grichelde", true)
|
|
|
|
Grichelde.version = GetAddOnMetadata(AddonName, "Version")
|
|
|
|
Grichelde.build = GetAddOnMetadata(AddonName, "X-Build") or "Experimental"
|
|
|
|
Grichelde.hooks = {}
|
|
|
|
Grichelde.classic = _G.WOW_PROJECT_ID == _G.WOW_PROJECT_CLASSIC
|
|
|
|
Grichelde.debug = false
|
|
|
|
|
|
|
|
-- publish to global env
|
|
|
|
_G.Grichelde = Grichelde
|
|
|
|
|
|
|
|
Grichelde.config = {
|
|
|
|
enabled = true,
|
|
|
|
channels = { "SAY", "EMOTE", "YELL", "PARTY", "GUILD" },
|
|
|
|
slashCommands = { "/s", "/say", "/e", "/em", "/me", "/emote", "/y", "/yell", "/sh", "/shout", "/p", "/party", "/pl", "/partyleader", "/g", "/gc", "/guild", "/o", "/osay", "/officer", "/raid", "/rsay", "/rl", "/raidleader", "/rw", "/raidwarning", "/i", "/instance", "/bg", "/battleground", "/w", "/whisper", "/t", "/tell", "/send", "/r", "/reply" }
|
|
|
|
}
|
|
|
|
|
|
|
|
-- Ace3 callbacks
|
|
|
|
function Grichelde:OnInitialize()
|
|
|
|
-- Build Interface Options window
|
|
|
|
self:LoadDatabase()
|
|
|
|
self:SetupOptions()
|
|
|
|
self:SetupSlashCommands()
|
|
|
|
|
|
|
|
-- 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 (_G.Misspelled) then
|
|
|
|
self:PrefixedPrint(self.L.Addon_Detected_Misspelled)
|
|
|
|
end
|
|
|
|
|
|
|
|
-- tell the world we are listening
|
|
|
|
self:DebugPrint(self.L.AddonLoaded, self.L.AddonName)
|
|
|
|
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
|
|
|
|
|
|
|
|
if (_G.WIM) then
|
|
|
|
self:PrefixedPrint(self.L.Addon_Detected_WIM)
|
|
|
|
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)
|
|
|
|
if (not Grichelde.config.enabled) then
|
|
|
|
self:DebugPrint("CheckReplacement : globally disabled")
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
|
|
|
-- check type
|
|
|
|
if (not tContains(Grichelde.config.channels, 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.config.slashCommands, 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)
|
|
|
|
|
|
|
|
-- 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 = string_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(Grichelde.config.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 <= string_len(newText) do
|
|
|
|
local currentChar = string_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 = 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
|
|
|
|
-- todo: consolidate consecutive
|
|
|
|
-- todo: prevent infinite loops - is that even possible?
|
|
|
|
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 \"%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 = 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: %s, start: %d", text, start)
|
|
|
|
local pos = start or 1
|
|
|
|
local left, right = strmatch(text, "^.- .+", pos)
|
|
|
|
self:DebugPrint("SplitOnFirstMatch : left: %s, right: %s", left, right)
|
|
|
|
return left, right
|
|
|
|
end
|
|
|
|
|
|
|
|
function Grichelde:SplitOnLastMatch(text, start)
|
|
|
|
self:DebugPrint("SplitOnLastMatch : text: %s, start: %d", text, start)
|
|
|
|
local pos = start or 1
|
|
|
|
local left, right = strmatch(text, ".+ .-$", pos)
|
|
|
|
self:DebugPrint("SplitOnLastMatch : left: %s, right: %s", left, right)
|
|
|
|
return left, right
|
|
|
|
end
|
|
|
|
|
|
|
|
function Grichelde:Format(message,...)
|
|
|
|
local msg = message
|
|
|
|
local l = select("#", ...)
|
|
|
|
if l > 0 then
|
|
|
|
-- sanitize nil values in vararg
|
|
|
|
local packed = {...}
|
|
|
|
for i = 1,l do
|
|
|
|
packed[i] = packed[i] or "nil"
|
|
|
|
end
|
|
|
|
-- print("packed = ", packed)
|
|
|
|
-- self:tPrint(packed)
|
|
|
|
-- cannot assign unpacked to a vararg variable and print it for debug
|
|
|
|
msg = string_fmt(message, unpack(packed))
|
|
|
|
end
|
|
|
|
return msg or "nil"
|
|
|
|
end
|
|
|
|
|
|
|
|
function Grichelde:Print(...)
|
|
|
|
print(self:Format(...))
|
|
|
|
end
|
|
|
|
|
|
|
|
local function prefixedPrint(colorCode, prefix, endClose, ...)
|
|
|
|
print(colorCode .. prefix .. endClose .. ": " .. ...)
|
|
|
|
end
|
|
|
|
|
|
|
|
function Grichelde:PrefixedPrint(...)
|
|
|
|
prefixedPrint(PREFIX_COLOR_CODE, self.L.AddonName, _G.FONT_COLOR_CODE_CLOSE, self:Format(...))
|
|
|
|
end
|
|
|
|
|
|
|
|
function Grichelde:DebugPrint(...)
|
|
|
|
if (self.debug) then
|
|
|
|
prefixedPrint(_G.GRAY_FONT_COLOR_CODE, self.L.AddonName, _G.FONT_COLOR_CODE_CLOSE, self:Format(...))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local function tLen(t)
|
|
|
|
local count = 0
|
|
|
|
for _ in pairs(t) do count = count + 1 end
|
|
|
|
return count
|
|
|
|
end
|
|
|
|
|
|
|
|
-- show strings differently to distinguish them from numbers
|
|
|
|
function Grichelde:PlainValue(val)
|
|
|
|
if val == nil then
|
|
|
|
return "<nil>"
|
|
|
|
elseif type(val) == "string" then
|
|
|
|
return '"' .. val .. '"'
|
|
|
|
elseif type(val) == "table" then
|
|
|
|
if tLen(val) > 0 then
|
|
|
|
return tostring(val)
|
|
|
|
else
|
|
|
|
return "{}"
|
|
|
|
end
|
|
|
|
else
|
|
|
|
return tostring(val)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
--- Prints any value to default channel, do NOT return a string.
|
|
|
|
function Grichelde:tPrint(val, indent, known)
|
|
|
|
if (not self.debug) then return end
|
|
|
|
|
|
|
|
indent = indent or 0
|
|
|
|
known = known or {}
|
|
|
|
|
|
|
|
if val == nil then
|
|
|
|
print(string_rep(" ", indent) .. "<nil>")
|
|
|
|
elseif type(val) == "string" then
|
|
|
|
print(string_rep(" ", indent) .. "\"" .. val .. "\"")
|
|
|
|
elseif type(val) == "table" then
|
|
|
|
if tLen(val) > 0 then
|
|
|
|
for key, value in pairs(val) do
|
|
|
|
if value == nil then
|
|
|
|
print(string_rep(" ", indent) .. self:PlainValue(key) .. "= <nil>")
|
|
|
|
elseif type(value) == "table" then
|
|
|
|
print(string_rep(" ", indent) .. self:PlainValue(key) .. "= {")
|
|
|
|
if tLen(value) > 0 then
|
|
|
|
if not known[value] then
|
|
|
|
self:tPrint(value, indent + 4, known)
|
|
|
|
known[value] = true
|
|
|
|
else
|
|
|
|
print("<known table> " .. self:PlainValue(value))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
print(string_rep(" ", indent) .. "}")
|
|
|
|
else
|
|
|
|
print(string_rep(" ", indent) .. self:PlainValue(key) .. " = ".. self:PlainValue(value))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
else
|
|
|
|
print(string_rep(" ", indent) .. "{}")
|
|
|
|
end
|
|
|
|
else
|
|
|
|
print(string_rep(" ", indent) .. tostring(val))
|
|
|
|
end
|
|
|
|
end
|