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.
1006 lines
41 KiB
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 |