You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Grichelde/GricheldeChat.lua

1006 lines
41 KiB
Lua

-- import addon read namespace from global env
local _G = _G
local Grichelde = _G.Grichelde or {}
local IsAddOnLoaded, assert, nilOrEmpty, pairs, ipairs, spairs, tContains, tFilter, tInsert, tIsEmpty, find, sub, gsub, getNextCharUtf8, isUtf8MultiByte, getUtf8Sequence, isUpper, isLower, toUpper, toLower, bytes2Char, trim, length, lengthUtf8
= Grichelde.F.IsAddOnLoaded, Grichelde.F.assert, Grichelde.F.nilOrEmpty, Grichelde.F.pairs, Grichelde.F.ipairs, Grichelde.F.spairs, Grichelde.F.tContains, Grichelde.F.tFilter, Grichelde.F.tInsert, Grichelde.F.tIsEmpty,
Grichelde.F.find, Grichelde.F.sub, Grichelde.F.gsub, Grichelde.F.getNextCharUtf8, Grichelde.F.isUtf8MultiByte, Grichelde.F.getUtf8Sequence, Grichelde.F.isUpper, Grichelde.F.isLower, Grichelde.F.toUpper, Grichelde.F.toLower, Grichelde.F.bytes2Char, Grichelde.F.trim, Grichelde.F.length, Grichelde.F.lengthUtf8
--- Splits a long text in longest possible chunks of <= 255 length, split at last available space
-- @param text string
-- @return array of chunks
function Grichelde:SplitText(text)
local chunks = {}
local leftGuillemet = bytes2Char(194, 171) .. " "
local rightGuillemet = " " .. bytes2Char(194, 187)
local chunkSize = Grichelde.INPUT_LIMIT - length(leftGuillemet) - length(rightGuillemet)
local function preserveText(newText, chunk, blockText, posEnd)
-- link found, block completed
self:TracePrint("SplitText : Found preservable text up to %s", posEnd)
local preserved = sub(newText, 1, posEnd)
if ((length(chunk) > 0) and (length(chunk .. blockText) > chunkSize)) then
-- block exceeds chunk, chunkify previous blocks
self:DebugPrint("SplitText : add chunk:", chunk)
tInsert(chunks, chunk .. rightGuillemet)
chunk = leftGuillemet .. trim(blockText)
else
chunk = chunk .. blockText
end
if ((length(chunk) > 0) and (length(chunk .. preserved) > chunkSize)) then
-- block exceeds chunk, chunkify previous blocks
self:DebugPrint("SplitText : add chunk:", chunk)
tInsert(chunks, chunk .. rightGuillemet)
chunk = leftGuillemet .. trim(preserved)
else
chunk = chunk .. preserved
end
blockText = ""
newText = sub(newText, posEnd + 1)
return newText, chunk, blockText, posEnd
end
if (length(text or "") <= Grichelde.INPUT_LIMIT) then
self:DebugPrint("SplitText : no chunk:", text)
tInsert(chunks, text)
else
local lookAheads = { '|', '*', '<', '%', '{', '(', 'o' }
local newText = text or ""
local chunk, blockText = "", ""
local currentChar
local escape = 0
-- must not enforce UTF-8 support here, as the positions are used
while ((length(newText) > 0) and (escape < Grichelde.ENDLESS_LOOP_LIMIT)) do
escape = escape + 1
local first, textAhead = getNextCharUtf8(newText)
currentChar = first
self:DebugPrint("SplitText : currentChar, escape: %s, %s", currentChar, escape)
self:TracePrint("SplitText : chunk:", chunk)
self:TracePrint("SplitText : newText:", newText)
-- as there is not OR in Luas pattern matching, search for all of the exclude patterns after another is
-- cumbersome and inefficient -> look for each char consecutively if it matches the starting pattern only
-- and if if matches do full pattern matching
if (currentChar == ' ') then
self:TracePrint("SplitText : block completed")
if ((length(chunk) > 0) and (length(chunk .. blockText) > chunkSize)) then
-- block exceeds chunk, chunkify previous blocks
self:DebugPrint("SplitText : add chunk:", chunk)
tInsert(chunks, chunk .. rightGuillemet)
chunk = leftGuillemet .. trim(blockText)
else
chunk = chunk .. blockText
end
blockText = currentChar
newText = textAhead
elseif (tContains(lookAheads, currentChar)) then
-- lookahead-check for all preservable patterns (itemLinks, textures, emotes, ooc, etc.)
-- link detection
local linkPosEnd = self:CheckForLink(newText, currentChar)
if (linkPosEnd ~= nil) then
-- link found, block completed
newText, chunk, blockText = preserveText(newText, chunk, blockText, linkPosEnd)
else
-- substitution detection
local substPosEnd = self:CheckForSubstitutions(newText, currentChar)
if (substPosEnd ~= nil) then
-- substitution found, block completed
newText, chunk, blockText = preserveText(newText, chunk, blockText, substPosEnd)
else
-- raid target marker detection
local rtmPosEnd = self:CheckForRaidTargetMarkers(newText, currentChar)
if (rtmPosEnd ~= nil) then
-- raid target marker found, block completed
newText, chunk, blockText = preserveText(newText, chunk, blockText, rtmPosEnd)
else
blockText = blockText .. currentChar
newText = textAhead
end
end
end
else
blockText = blockText .. currentChar
newText = textAhead
end
end
self:TracePrint("SplitText : main loop completed")
if (length(chunk .. blockText) > 0) then
-- catchup remaining text at the end
if (length(chunk .. blockText) > chunkSize) then
-- block exceeds chunk, chunkify previous blocks
if (length(chunk) > 0) then
self:DebugPrint("SplitText : add chunk:", chunk)
tInsert(chunks, chunk .. rightGuillemet)
chunk = leftGuillemet .. trim(blockText)
else
chunk = chunk .. blockText
end
else
chunk = chunk .. blockText
end
self:DebugPrint("SplitText : last chunk:", chunk)
-- sub(chunk, 1, 255) can result in broken UTF8 chars and error message
tInsert(chunks, chunk)
end
end
return chunks
end
--- Send text in chunks if length exceeds 255 bytes after replacement.
function Grichelde:SendChunkifiedChatMessage(message, ...)
if (length(message) > 255) then
local chunks = self:SplitText(message)
self:DebugPrint("SendChunkifiedChatMessage : #chunks:", #chunks)
for _, chunk in ipairs(chunks) do
self.hooks["SendChatMessage"](chunk, ...);
end
else
self.hooks["SendChatMessage"](message, ...);
end
end
function Grichelde:ReplaceCharacters(text, replName, replTable, consolidate, replacedTexts)
local function convertToCaseInsensitivePatternGroup(p)
local upperP, lowerP = toUpper(p), toLower(p)
--[[
if (isUtf8MultiByte(p)) then
local sequence = nil
for _, byteSequence in spairs(getUtf8Table(upperP)) do
sequence = sequence .. byteSequence
end
else
]]
if (upperP ~= lowerP) then
return upperP .. lowerP
else
return p
end
--end
end
local function convertToCaseInsensitivePattern(pattern)
local ciPattern = ""
local ignored = {'^', '$', '(', ')', '.'}
local quantifiers = {'*', '+', '-', '?'}
local p, patRest = getNextCharUtf8(pattern)
local escape = 0
while ((p ~= nil) and (escape < Grichelde.ENDLESS_LOOP_LIMIT)) do
escape = escape + 1
if (tContains(ignored, p) or tContains(quantifiers, p)) then
-- ignore
ciPattern = ciPattern .. p
elseif (p == "%") then
-- ignore capture references
p, patRest = getNextCharUtf8(patRest)
if (p ~= nil) then
ciPattern = ciPattern .. "%" .. p
end
elseif (p == "[") then
-- skip pattern sets
ciPattern = ciPattern .. "["
p, patRest = getNextCharUtf8(patRest)
while ((p ~= nil) and (p ~= "]")) do
if (p == "%") then
-- ignore capture references
p, patRest = getNextCharUtf8(patRest)
if (p ~= nil) then
ciPattern = ciPattern .. "%" .. p
end
else
ciPattern = ciPattern .. convertToCaseInsensitivePatternGroup(p)
end
p, patRest = getNextCharUtf8(patRest)
end
ciPattern = ciPattern .. "]"
else
ciPattern = ciPattern .. "[" .. convertToCaseInsensitivePatternGroup(p) .. "]"
end
p, patRest = getNextCharUtf8(patRest)
end
self:TracePrint("convertToCaseInsensitivePattern : %s => %s", pattern, ciPattern)
return ciPattern
end
local function replaceCaptures(txt, replaceText, captures)
local replText = replaceText
self:TracePrint("replaceCaptures : txt: %s, #captures: %d", txt, #captures)
if (#captures > 0) then
for i, cap in ipairs(captures) do
--self:TracePrint("replaceCaptures : i: %d, cap: %s", i, cap)
if (cap == nil) then
break
else
local oldRepl = replText
replText = gsub(oldRepl, "%%" .. i, cap)
self:TracePrint("ReplaceCaptures : substitute capture %s: %s => %s", oldRepl, cap, replText)
end
end
else
self:TracePrint("ReplaceCaptures : no captures")
end
return replText
end
--- this is more complicated to get it right than it looks like
local function applyCase(replRest, lastCase, nextCase)
local repl = ""
if (lastCase == nil) then
-- lastCase was unknown, always take over nextCase
if (nextCase == nil) then
repl = repl .. replRest
elseif (nextCase == true) then
--repl = repl .. toUpper(replRest)
repl = repl .. replRest
else
--repl = repl .. toLower(replRest)
repl = repl .. replRest
end
elseif (lastCase == true) then
-- lastCase was UPPER
if (nextCase == nil) then
repl = repl .. toUpper(replRest)
elseif (nextCase == true) then
repl = repl .. toUpper(replRest)
else
repl = repl .. replRest
end
else
-- lastCase was lower
if (nextCase == nil) then
repl = repl .. replRest
elseif (nextCase == true) then
--repl = repl .. toLower(replRest)
repl = repl .. replRest
else
--repl = repl .. toLower(replRest)
repl = repl .. replRest
end
end
return repl
end
local pos = 1
local result = text
local findText = result
local searchText = replTable.searchText
local replaceText = replTable.replaceText
local matchWhen = replTable.matchWhen
local doExactCase = replTable.exactCase
local doConsolidate = replTable.consolidate
local doStopOnMatch = replTable.stopOnMatch
local stopOnMatch = false
if doExactCase then
self:DebugPrint("ReplaceCharacters : \"%s => %s\" (exact case)", searchText, replaceText)
else
self:DebugPrint("ReplaceCharacters : \"%s => %s\" (ignoreCase)", searchText, replaceText)
searchText = convertToCaseInsensitivePattern(searchText)
end
local pos1, pos2, cap1, cap2, cap3, cap4, cap5, cap6, cap7, cap8, cap9 = find(findText, searchText, pos)
self:TracePrint("ReplaceCharacters : pos1: %d, pos2: %d", pos1, pos2)
self:TracePrint("ReplaceCharacters : cap1: %s, cap2: %s, cap3: %s, cap4: %s, cap5: %s, cap6: %s, cap7: %s, cap8: %s, cap9: %s", cap1, cap2, cap3, cap4, cap5, cap6, cap7, cap8, cap9)
while (pos1 ~= nil) and (pos2 ~= nil) and (pos1 <= pos2) do
if doStopOnMatch then
stopOnMatch = true
end
local pre = sub(result, 1, pos1 - 1)
local match = sub(result, pos1, pos2)
local post = sub(result, pos2 + 1)
local wordStart = sub(pre, -1, -1)
local wordEnd = sub(post, 1, 1)
self:TracePrint("ReplaceCharacters : result: %s", result)
self:TracePrint("ReplaceCharacters : pre: %s, match: %s, post: %s, wordStart: %s, wordEnd: %s", pre, match, post, wordStart, wordEnd)
--[[
self:TracePrint("ReplaceCharacters : pos2: %d, isChar: %s", pos2, isChar(match))
while (not isChar(match) and pos2 < length(result)) do
pos2 = pos2 + 1
pre = sub(result, 1, pos1 - 1)
match = sub(result, pos1, pos2)
post = sub(result, pos2 + 1)
wordStart = sub(pre, -1, -1)
wordEnd = sub(post, 1, 1)
self:TracePrint("ReplaceCharacters : pos2: %d, isChar: %s", pos2, isChar(match))
end
]]
-- continue from that position later
pos = pos2 + 1
-- additional checks for word boundaries
local doesMatchWhen = false
if (matchWhen == 2) then
-- replace always
doesMatchWhen = true
elseif (matchWhen == 3) then
-- replace only as a whole word
if (nilOrEmpty(wordStart) or (find(wordStart,"[%s%p]") ~= nil)) and (nilOrEmpty(wordEnd) or (find(wordEnd, "[%s%p]") ~= nil)) then
doesMatchWhen = true
end
elseif (matchWhen == 4) then
-- replace only at start
if (nilOrEmpty(wordStart) or (find(wordStart,"[%s%p]") ~= nil)) then
doesMatchWhen = true
end
elseif (matchWhen == 5) then
-- replace only at end
if (nilOrEmpty(wordEnd) or (find(wordEnd, "[%s%p]") ~= nil)) then
doesMatchWhen = true
end
elseif (matchWhen == 6) then
-- replace only at start or end
if (nilOrEmpty(wordStart) or (find(wordStart, "[%s%p]") ~= nil) or nilOrEmpty(wordEnd) or (find(wordEnd, "[%s%p]") ~= nil)) then
doesMatchWhen = true
end
elseif (matchWhen == 7) then
-- replace only in the middle
if (not nilOrEmpty(wordStart) and (find(wordStart, "[%w]") ~= nil) and not nilOrEmpty(wordEnd) and (find(wordEnd, "[%w]") ~= nil)) then
doesMatchWhen = true
end
end
if (doesMatchWhen) then
-- replace substitutions
self:TracePrint("ReplaceCharacters : pre: %s, match: %s, post: %s, repl: %s", pre, match, post, replaceText)
local caps = { cap1, cap2, cap3, cap4, cap5, cap6, cap7, cap8, cap9 }
local replText = replaceCaptures(match, replaceText, caps)
tInsert(replacedTexts[replName], replText)
self:DebugPrint("ReplaceCharacters : pre: %s, match: %s, post: %s, repl: %s", pre, match, post, replText)
if (not doExactCase) then
local repl, lastCase = "", nil
local c, matchRest = getNextCharUtf8(match)
local r, replRest = "", replText
while (c ~= nil) do
r, replRest = getNextCharUtf8(replRest)
r = r or ""
replRest = replRest or ""
self:TracePrint("ReplaceCharacters : c: %s, rest: %s", c, matchRest)
self:TracePrint("ReplaceCharacters : r: %s, rest: %s", r, replRest)
if (isUpper(c)) then
-- UPPER-CASE letter
self:TracePrint("ReplaceCharacters : characters: %s => %s", c, toUpper(r))
lastCase = true
repl = repl .. toUpper(r)
elseif (isLower(c)) then
-- lower_case letter
self:TracePrint("ReplaceCharacters : characters: %s => %s", c, toLower(r))
lastCase = false
repl = repl .. toLower(r)
else
-- no letter
self:TracePrint("ReplaceCharacters : characters: %s => %s", c, r)
lastCase = nil
repl = repl .. r
end
c, matchRest = getNextCharUtf8(matchRest)
end
self:TracePrint("ReplaceCharacters : remaining length %d", length(replRest))
if (length(replRest) > 0) then
local nextLetter, _ = getNextCharUtf8(post)
nextLetter = nextLetter or ""
local nextCase = nil
if (isUpper(nextLetter)) then
nextCase = true
elseif (isLower(nextLetter)) then
nextCase = false
end
self:TracePrint("ReplaceCharacters : rest: %s, lastCase: %s, nextLetter: %s, nextCase: %s", replRest, lastCase, nextLetter, nextCase)
repl = repl .. applyCase(replRest, lastCase, nextCase)
end
replText = repl
end
-- actual replacement
result = pre .. replText .. post
self:TracePrint("ReplaceCharacters : result: %s", result)
-- remember positions for consolidate
if doConsolidate then
tInsert(consolidate[replName], pos1)
end
-- update previous consolidate markers
local diff = lengthUtf8(replText) - lengthUtf8(match)
self:TracePrint("ReplaceCharacters : diff = %d - %d", lengthUtf8(replText), lengthUtf8(match))
for key, posList in pairs(consolidate) do
if (key ~= replName) then
for i, pc in ipairs(posList) do
if (pos1 < pc) then
consolidate[key][i] = consolidate[key][i] + diff
end
end
end
end
-- replacement text can lengthen or shorten the result
-- after replacement text and lowerText can have different sizes
pos = pos + diff
else
self:DebugPrint("ReplaceCharacters : does not match when: %d", matchWhen)
end
findText = result
-- update values for next iteration
pos1, pos2, cap1, cap2, cap3, cap4, cap5, cap6, cap7, cap8, cap9 = find(findText, searchText, pos)
self:TracePrint("ReplaceCharacters : pos1: %d, pos2: %d", pos1, pos2)
self:TracePrint("ReplaceCharacters : cap1: %s, cap2: %s, cap3: %s, cap4: %s, cap5: %s, cap6: %s, cap7: %s, cap8: %s, cap9: %s", cap1, cap2, cap3, cap4, cap5, cap6, cap7, cap8, cap9)
end
if (text ~= result) then
self:DebugPrint("ReplaceCharacters : replaced \"%s\" with \"%s\" %s", text, result, stopOnMatch and "(stop)" or "")
end
if (matchWhen > 1) and doConsolidate then
self:DebugPrint("ReplaceCharacters : consolidate[" .. replName .. "] is:")
self:DebugPrint(consolidate[replName])
end
return result, stopOnMatch
end
--- Replaces all character occurrences for which replacements have been defined in the options
-- @param text string
-- @param replacements table of mappings
-- @return string
function Grichelde:ReplaceAndConsolidate(text, _replacements)
local replacements = _replacements or self.db.profile.replacements or {}
self:TracePrint("ReplaceAndConsolidate : replacements")
self:TracePrint(replacements)
local result = text
local consolidate = {}
local replacedTexts = {}
local stopOnMatch = nil
-- replacements are done first
for replName, replTable in spairs(replacements) do
local searchText = replTable.searchText
if (not nilOrEmpty(searchText) and (replTable.matchWhen > 1)) then
consolidate[replName] = {}
replacedTexts[replName] = {}
local stop = false
result, stop = self:ReplaceCharacters(result, replName, replTable, consolidate, replacedTexts)
if stop then
stopOnMatch = replName
self:DebugPrint("ReplaceAndConsolidate : Stopping followup replacements after %s", replName)
break
end
else
-- empty mapping or never matched
self:DebugPrint("ReplaceAndConsolidate : Skip replacement %s", replName)
end
end
-- consolidation is done last
for replName, replTable in spairs(replacements) do
local before = result
local search = replTable.searchText
if (not nilOrEmpty(search) and (replTable.matchWhen > 1)) then
local lowerResult = toLower(result)
local offset = 0
if (replTable.consolidate) then
self:DebugPrint("consolidate[" .. replName .. "] is:")
self:DebugPrint(consolidate[replName])
self:DebugPrint("replacedTexts[" .. replName .. "] is:")
self:DebugPrint(replacedTexts[replName])
for i, pos1 in spairs(consolidate[replName]) do
local replText = replacedTexts[replName][i]
self:DebugPrint("ReplaceAndConsolidate : consolidating \"%s => %s\"", search, replText)
local pos2 = pos1 + length(replText) - 1
self:TracePrint("ReplaceAndConsolidate : pos1: %d, pos2: %d", pos1, pos2)
local match = toLower(replText)
local next = sub(lowerResult, pos2 + 1, pos2 + 1 + pos2 - pos1)
self:TracePrint("ReplaceAndConsolidate : match: %s, next: %s", match, next)
local _, p2 = find(next, "^" .. match)
self:TracePrint("ReplaceAndConsolidate : p2: %d", p2)
if (p2 ~= nil) 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("ReplaceAndConsolidate : result: %s", result)
end
end
if (before ~= result) then
self:DebugPrint("ReplaceAndConsolidate : consolidate \"%s\" with \"%s\"", before, result)
end
else
self:DebugPrint("ReplaceAndConsolidate : Skip consolidation for %s", replName)
end
if (stopOnMatch ~= nil) and (stopOnMatch == replName) then
break
end
end
self:DebugPrint("ReplaceAndConsolidate : final text:", result)
return result
end
--- looks for colored items, item links or textures
function Grichelde:CheckForLink(text, currentChar)
if (currentChar == "|") then
for _, pattern in ipairs(Grichelde.IGNORE_PATTERNS.LINKS) do
local pos1, pos2 = find(text, "^" .. pattern)
if (pos1 == 1) and (pos2 ~= nil) then
self:DebugPrint("CheckForLink : Found link or texture pattern \"%s\" at (%d, %d)", pattern, pos1, pos2)
return pos2
end
end
end
return nil
end
--- looks for emotes
function Grichelde:CheckForEmote(text, currentChar, replaceEmotes)
if (currentChar == "*" or currentChar == "<") then
for _, pattern in ipairs(Grichelde.IGNORE_PATTERNS.EMOTES) do
local pos1, pos2 = find(text, "^" .. pattern)
if (pos1 == 1) and (pos2 ~= nil) then
local emote = sub(text, pos1, pos2)
if (not replaceEmotes) then
self:DebugPrint("CheckForEmote : Found emote \"%s\" at (%d, %d), but preserved it", emote, pos1, pos2)
return pos2
else
self:DebugPrint("CheckForEmote : Processing emote \"%s\" at (%d, %d)", emote, pos1, pos2)
end
end
end
end
return nil
end
--- looks for %-substitutions
function Grichelde:CheckForSubstitutions(text, currentChar)
local lowerText = toLower(text)
if (currentChar == "%") then
for _, pattern in ipairs(Grichelde.IGNORE_PATTERNS.SUBSTITUTES) do
local pos1, pos2 = find(lowerText, "^" .. pattern)
if (pos1 == 1) and (pos2 ~= nil) then
self:DebugPrint("CheckForPreversableText : Found substitute pattern \"%s\" at (%d, %d)", pattern, pos1, pos2)
return pos2
end
end
end
return nil
end
--- looks for general and localized raid target markers
function Grichelde:CheckForRaidTargetMarkers(text, currentChar)
local lowerText = toLower(text)
if (currentChar == "{") then
-- rt1-9
local pattern = Grichelde.IGNORE_PATTERNS.RAID_TARGETS[1]
local pos1, pos2 = find(lowerText, "^" .. pattern)
if (pos1 == 1) and (pos2 ~= nil) then
self:DebugPrint("CheckForPreversableText : Found raid target marker \"%s\" at (%d, %d)", pattern, pos1, pos2)
return pos2
end
for _, localizedRT in ipairs(Grichelde.IGNORE_PATTERNS.LOCALIZED_RAID_TARGETS) do
local translation = toLower(self.L["IgnorePattern_" .. localizedRT])
local localizedPattern = "^{" .. translation .. "}"
self:TracePrint("CheckForPreversableText : localizedPattern:", localizedPattern)
local p1, p2 = find(lowerText, localizedPattern)
if (p1 == 1) and (p2 ~= nil) then
self:DebugPrint("CheckForPreversableText : Found localized raid target marker \"%s\" at (%d, %d)", localizedPattern, p1, p2)
return p2
end
end
end
return nil
end
--- looks for ooc with brackets
function Grichelde:CheckForOocBrackets(text, currentChar)
local lowerText = toLower(text)
if (currentChar == "(") then
for _, pattern in ipairs(Grichelde.IGNORE_PATTERNS.OOC_BRACKETS) do
local pos1, pos2 = find(lowerText, "^" .. pattern)
if (pos1 == 1) and (pos2 ~= nil) then
self:DebugPrint("CheckForPreversableText : Found ooc pattern \"%s\" at (%d, %d)", pattern, pos1, pos2)
return pos2
end
end
end
return nil
end
--- looks for ooc without brackets
function Grichelde:CheckForOocNoBrackets(text, currentChar, previousChar)
local lowerText = toLower(text)
if (currentChar == "o") then
local pattern = Grichelde.IGNORE_PATTERNS.OOC_NO_BRACKETS[1]
if ((previousChar == nil) or (find(previousChar, "%s") ~= nil)) and (find(lowerText, pattern) ~= nil) then
self:DebugPrint("CheckForPreversableText : ooc for remaing text")
-- remaing text is treated as ooc completely!
return length(text)
end
end
return nil
end
--- Checks if the text starts with a preversable ignore pattern, such as itemLinks, textures, raid target icons,
--- emotes, ooc or %-substitutons and returns the end location of the match, or 0 if no pattern was found
-- @param text string
-- @param currentChar string(1) current character (first one) of the text, given for performance reasons
-- @param previousChar string(1) previous character of the text, otherwise unreachable
-- @param preserveEmotes boolean ignore replacements for emotes, for testing purposes
-- @return number
function Grichelde:CheckForPreversableText(text, currentChar, previousChar, _replaceEmotes)
self:DebugPrint("CheckForPreversableText : text:", text)
local replaceEmotes = _replaceEmotes or self.db.profile.channels.emote or false
local linkPos = self:CheckForLink(text, currentChar)
if (linkPos ~= nil) then
return linkPos
end
local emotePos = self:CheckForEmote(text, currentChar, replaceEmotes)
if (emotePos ~= nil) then
return emotePos
end
local substPos = self:CheckForSubstitutions(text, currentChar)
if (substPos ~= nil) then
return substPos
end
local rtmPos = self:CheckForRaidTargetMarkers(text, currentChar)
if (rtmPos ~= nil) then
return rtmPos
end
local oocBracketPos = self:CheckForOocBrackets(text, currentChar)
if (oocBracketPos ~= nil) then
return oocBracketPos
end
local oocNoBracketPos = self:CheckForOocNoBrackets(text, currentChar, previousChar)
if (oocNoBracketPos ~= nil) then
return oocNoBracketPos
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 the text to apply the mappings on
-- @param preserveEmotes boolean ignore replacements for emotes, for testing purposes
-- @return string
function Grichelde:ReplaceText(text, _replacements, _replaceEmotes)
local lookAheads = { '|', '*', '<', '%', '{', '(', 'o' }
local newText = text
local preserveEmotes = _replaceEmotes or self.db.profile.channels.emote or false
local replacements = _replacements or self.db.profile.replacements or {}
local finalText, replaceText = "", ""
local currentChar
local escape = 0
-- must not enforce UTF-8 support here, as the positions are used
while ((length(newText) > 0) and (escape < Grichelde.ENDLESS_LOOP_LIMIT)) do
escape = escape + 1
local previousChar = currentChar
local first, textAhead = getNextCharUtf8(newText)
currentChar = first
self:TracePrint("ReplaceText : currentChar : %s", currentChar)
-- as there is not OR in Luas pattern matching, search for all of the exclude patterns after another is
-- cumbersome and inefficient -> look for each char consecutively if it matches the starting pattern only
-- and if if matches do full pattern matching
if (tContains(lookAheads, currentChar)) then
-- lookahead-check for all preservable patterns (itemLinks, textures, emotes, ooc, etc.)
--local textAhead = sub(newText, current)
local posEnd = self:CheckForPreversableText(newText, currentChar, previousChar, preserveEmotes)
if (posEnd > 0) then
self:DebugPrint("ReplaceText : Found an ignore pattern")
-- replace all text up until now
local replacement = self:ReplaceAndConsolidate(replaceText, replacements)
local preserved = sub(newText, 1, posEnd)
finalText = finalText .. replacement .. preserved
replaceText = ""
newText = sub(newText, posEnd + 1)
self:DebugPrint("ReplaceText : remaining text", newText)
else
-- no corresponding end was found to start pattern, continue loop with next char
replaceText = replaceText .. currentChar
newText = textAhead
end
else
replaceText = replaceText .. currentChar
newText = textAhead
end
end
-- catchup remaining text to the end
local replacement = self:ReplaceAndConsolidate(replaceText, replacements)
finalText = finalText .. replacement
self:DebugPrint("ReplaceText : replaced \"%s\"", text)
self:DebugPrint("ReplaceText : with \"%s\"", finalText)
return finalText
end
function Grichelde:IsOneBigEmote(text)
local firstWord, _ = self:SplitOnFirstMatch(text)
assert(firstWord ~= nil, "firstWord is never nil")
-- emote detection
local isEmote = false
local firstChar, rest = getNextCharUtf8(firstWord)
-- scheme <emote>
if (firstChar == "<") then
-- search for emote end
local _, emoteEnd = find(text, "%>", 2)
isEmote = (emoteEnd == lengthUtf8(text))
end
if (not isEmote and (firstChar == "*")) then
if (getNextCharUtf8(rest) == "*") then
-- scheme **emote**
local _, emoteEnd = find(text, "%*%*", 3)
isEmote = (emoteEnd == lengthUtf8(text))
else
-- scheme *emote*
local _, emoteEnd = find(text, "%*", 2)
isEmote = (emoteEnd == lengthUtf8(text))
end
end
-- the whole text is one big emote
return isEmote
end
function Grichelde:ConvertBlizzTypeToOption(channel)
local option = tFilter(self.BLIZZ_TYPE_TO_OPTIONS,
function(_, k, _) return channel == k end)
if (not tIsEmpty(option)) then
self:DebugPrint("ConvertBlizzTypeToOption : convert %s to %s", channel, option[1])
return option[1]
else
return nil
end
end
--- Checks if a message can be replaced according to configuration.
-- @return boolean
function Grichelde:CheckReplacementAllowed(text, channel)
self:DebugPrint("CheckReplacementAllowed : text:", text)
-- skip if disabled
if (not self.db.profile.enabled) then
self:DebugPrint("CheckReplacementAllowed : disabled")
return false
end
-- skip if no text
if (nilOrEmpty(text)) then
return false
end
-- skip if wrong channel
local chan = self:ConvertBlizzTypeToOption(channel)
local allowedChannels = tFilter(self.db.profile.channels,
function(_, k, v) return k == chan and v == true end,
function(_, k, _) return k end
)
self:DebugPrint("CheckReplacementAllowed : allowed channels:")
self:DebugPrint(allowedChannels)
if (tIsEmpty(allowedChannels)) then
self:DebugPrint("CheckReplacementAllowed : skip channel type:", chan)
return false
end
local firstWord, _ = self:SplitOnFirstMatch(text)
assert(firstWord ~= nil, "firstWord is never nil")
-- don't replace slash commands
if (getNextCharUtf8(firstWord) == "/") then
self:DebugPrint("CheckReplacementAllowed : skip other slash commands:", firstWord)
return false
end
-- emote detection
if (self:IsOneBigEmote(text)) then
self:DebugPrint("CheckReplacementAllowed : one big emote")
return self.db.profile.channels.emote
end
-- in any other case, treat as ordinary text or emote
return true
end
--- Checks if the text from the Grichelde slash command can be replaced and if so
--- returns the replacable text, the chat type and target (player or channel) from the message text.
--- self is used the the override slash command: "/gri /emote text to replace".
-- @param message (string) the whole message
-- @return message (string) stripped message
-- @return type (string) chat type
-- @return channel (string|number) channel number for whispers
function Grichelde:CheckAndExtractMessageTypeTarget(message)
self:DebugPrint("CheckAndExtractMessageTypeTarget : text:", message)
-- skip if no further text
if (nilOrEmpty(message)) then
-- dont send text at all
return nil
end
-- first word should be a chat command
if (getNextCharUtf8(message) == "/") then
-- extract chat command
local chatCmd, targetAndText = self:SplitOnFirstMatch(message)
assert(chatCmd ~= nil, "chatCmd is never nil")
local type = tFilter(self.SUPPORTED_CHAT_COMMANDS,
function(_, k, _) return chatCmd == k end
)
assert(#type < 2)
if (not tIsEmpty(type)) then
self:DebugPrint("CheckAndExtractMessageTypeTarget : determined type: %s", type[1])
-- valid /chattype
if (type[1] == "WHISPER") then
-- special reply handling
if ("/r" == chatCmd) or ("/reply" == chatCmd) then
-- reuse last type and target if possible
local lastTold, lastToldType = ChatEdit_GetLastToldTarget()
self:DebugPrint("CheckAndExtractMessageTypeTarget : lastTell, lastTellType =", lastTold, lastToldType)
return targetAndText, lastToldType or "WHISPER", lastTold
elseif ("/tt" == chatCmd) then
-- determine target from game world selection
if (not UnitExists("target") or not UnitIsPlayer("target")) then
self:ErrorPrint(self.L.Error_InvalidWhisperTarget)
-- dont send text at all
return nil
end
local target = UnitName("target");
if (target == nil) then
self:ErrorPrint(self.L.Error_InvalidWhisperTarget)
-- dont send text at all
return nil
end
-- eventually we found our target
self:DebugPrint("CheckAndExtractMessageTypeTarget : target:", target)
return targetAndText, "WHISPER", target
else
-- determine target from text
local target, text = self:SplitOnFirstMatch(targetAndText)
if (target == nil) then
self:ErrorPrint(self.L.Error_InvalidWhisperTarget)
-- dont send text at all
return nil
end
-- eventually we found our target
self:DebugPrint("CheckAndExtractMessageTypeTarget : determined target:", target)
return text, "WHISPER", target
end
else
-- all other chat types
return targetAndText, type[1], nil
end
else
self:DebugPrint("CheckAndExtractMessageTypeTarget : not a standard channel: %s", chatCmd)
-- if not a valid chat command, try as a numbered channel
local _, _, channelNumber = find(chatCmd, "^/(%d+)")
if (channelNumber ~= nil) then
local channelId = GetChannelName(channelNumber)
if (channelId ~= nil) then
return targetAndText, "CHANNEL", channelId
end
end
-- ignore any other slash commands
self:ErrorPrint(self.L.Error_InvalidChannel)
-- dont send text at all
return nil
end
elseif self:IsOneBigEmote(message) then
self:DebugPrint("CheckAndExtractMessageTypeTarget : determined EMOTE type")
return message, "EMOTE", nil
else
-- in any other case, treat as ordinary text, assume default type and channel
return message
end
end
function Grichelde:CleanseMessage(message)
local text = message or ""
if (IsAddOnLoaded("Misspelled")) then
self:DebugPrint("Misspelled detected: cleansing message")
text = _G.Misspelled:RemoveHighlighting(message)
end
return trim(text)
end
--- Always replaces the text accoording to the configuration, even if activation or channel was disabled.
--- self is used the the override slash command: "/gri /emote text to replace".
--- NOTE: type and channel (in case of whispers) are determined from the message text.
-- @param message string
-- @param type string
function Grichelde:SendChatMessageOverride(message, ...)
local cleasendText = self:CleanseMessage(message)
local fallbackType, fallbackLang = DEFAULT_CHAT_FRAME.editBox.chatType or "SAY", DEFAULT_CHAT_FRAME.editBox.languageID
local msg, type, chan = self:CheckAndExtractMessageTypeTarget(cleasendText)
if (msg ~= nil) then
msg = self:ReplaceText(msg)
if (type ~= nil) then
self:SendChunkifiedChatMessage(msg, type, fallbackLang, chan, ...)
else
self:SendChunkifiedChatMessage(msg, fallbackType, fallbackLang, chan, ...)
end
else
-- suppress invalid messages/channels/targets
end
end
--- 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, ...)
if (not self.db.profile.enabled) then
self:DebugPrint("SendChatMessage : disabled")
self.hooks["SendChatMessage"](message, type, ...);
elseif (nilOrEmpty(message)) then
self:DebugPrint("SendChatMessage : no text")
self.hooks["SendChatMessage"](message, type, ...);
else
local cleasendText = self:CleanseMessage(message)
if (self:CheckReplacementAllowed(cleasendText, type)) then
cleasendText = self:ReplaceText(cleasendText)
self:SendChunkifiedChatMessage(cleasendText, type, ...)
else
self.hooks["SendChatMessage"](message, type, ...);
end
end
end