-- 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 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