--[[-------------------------------------------------------------------------- Developed by Nathan Pieper - nrpieper (@) gmail (dot) com Misspelled is an interactive chat text spell checker for World of Warcraft This code freely distributed for your use in any GPL compliant project. Portions of this code are covered by the Gnu Public License (GPL) Dictionaries use the OpenOffice/HunSpell/ASpell dictionary format (http://wiki.services.openoffice.org/wiki/Dictionaries) Phonetic cache information created via the NetSpell dictionary util. References & Credits: Paul Welter & NetSpell: http://www.loresoft.com/projects/netspell Metaphone Algorithm http://aspell.net/metaphone/ Edit Distance Algorithm http://en.wikipedia.org/wiki/Levenshtein_distance --------------------------------------------------------------------------]]-- --[[ Challenges: Issue #1 (http://www.wowwiki.com/ItemLink) Color tags can be destroyed or mangled as you perform edit near the start and end of the color tag. If you press delete with the cursor positioned, just before, a color tag, the start of the tag (|cffffffff) and the next character are removed. The closing color tag (|r) is left at the end. If you press backspace with the cursor positioned just past the (|r) end of a color tag, the (|r) is removed along with the character just before it. If you press delete with the cursor positioned, just before the last char, in a color tagged string, the character and the closing (|r) are both removed. If you press backspace with the cursor positioned, just after the first char, in a color tagged string, the first char and the beginning color tag are both removed. In the case of colored item links, Wow just deletes the entire item, if you hit a delete or backspace. Solution: First parse the line to flag any valid item links or textures so they don't get destroyed by the next step. Then if there are any remaining start or end color tag, remove them. Issue #2 The Chat Edit box's edit cursor position is relative to characters that don't display in the chat edit text, i.e. item link tags, color tags, and texture tags. When we add, or remove, a color tag, to highlight a misspelled word, we should adjust the cursor position so it acts natural. Note: An OnCursorChanged event doesn't exist or doesn't fire for the ChatEditBox. Solution: Methods that insert or delete text from the ChatEditBox need to properly adjust the cursor position as needed when, visible or hidden, characters are inserted or deleted to the left of the cursor. Two techniques could be used. #1: Track the number of, printable and non-printable characters inserted or deleted to the left of the cursor, and adjust the cursor position to compensate. #2: Insert a tracking char byte (example: \1), at the cursor position. After making inserts and deletes, find the tracking char byte, remove it and reposition the cursor at its location. I have implemented technique #1. Issue #3 WIM Integration Wim allows multiple chat windows at once. So we need to track WordLocations per editbox. Wim breaks long chat messages into multiple messages. Wim has a EditBox right click handler to insert emoticons and previous chat messages. WIM sends chat messages using ChatThrottleLib:SendChatMessage. This can hot SendChatMessage before Misspelled and bypasses the step that cleans the highlighting. (10/10/2009) Updated hooks, from WIM, will be provided via WIM.RegisterPreSendFilterText(func()), to remove the need to manually hook ChatThrottleLib User Dictionary Editor Added (12/6/2009) - Used AceGUI to add the ability to remove words from the user dictionary. (1/12/2010) - Added enGB UK English dictionary. (5/12/2010) - Added support for PTR Patch 3.5. Multiple ChatEditBoxes get created. Hook into the activate routine. (5/23/2010) - Fixed enGB dictionary load issue. (6/24/2010) - Added itIT Italian dictionary (7/22/2010) - The Addon Gryphonheart Items (GHI) begins it's item color tags with a (|C) capitol C character. I added that possibility to the chat text parsing. (9/14/2010) - Client 4 has some issues with trying to set the owner of the popmenu items. Looks like it's not needed. (9/7/2019) - Wow Classic: When starting you can't have friends and the global function: GetNumFriends() is nil. Detect and skip to eliminate the error. (4/30/2025) - Changes added to RemoveHighlighting to parse new Item Quality # colors and Global Colors UI escape sequences. (12/18/2025) - Wow Retail 12.2.7 changes added to hook chat frames. --]]-- local _G = _G Misspelled = LibStub("AceAddon-3.0"):NewAddon("Misspelled", "AceEvent-3.0", "AceHook-3.0") local Misspelled = _G.Misspelled Misspelled.Version = GetAddOnMetadata("Misspelled", "Version") local L = LibStub("AceLocale-3.0"):GetLocale("Misspelled", true) local table_insert = table.insert local string_byte = string.byte local string_find = string.find local string_format = string.format local string_gsub = string.gsub local string_len = string.len local string_lower = string.lower local string_match = string.match local string_rep = string.rep local string_sub = string.sub local string_upper = string.upper local tostring = tostring local pairs = pairs local ipairs = ipairs --local SPELLED_WRONG_HIGHLIGHT = "|cffff91c8" --The color misspelled words will get changed into. --local SPELLED_WRONG_HIGHLIGHT = "|cffdea1d3" --The color misspelled words will get changed into. --local SPELLED_WRONG_HIGHLIGHT = "|cfffefe80" --The color misspelled words will get changed into. local SPELLED_WRONG_HIGHLIGHT_HEX_COLOR_CODE = "ff7dc6fb" --The color misspelled words will get changed into. (Medium-Cyan-ish) local SPELLED_WRONG_HIGHLIGHT = "|c"..SPELLED_WRONG_HIGHLIGHT_HEX_COLOR_CODE --The color misspelled words will get changed into. (Medium-Cyan-ish) local WordCache = {} --Stores a cache of every word checked, along with the suggestions for words misspelled local WordCacheCount = 0 --Counter used to track when we should clean the WordCache table to save memory local WordCacheCountMax = 7000 --Number of entries tha can live in the WordCache before we clean the cache local WordLocations = {} --Lookup table used by multiple functions to determine where each work starts and ends --There will be a sub-table for each EditBox:GetName() so we can store multiple sets of info at once. local SkipOnTextChanged = false -- use to avoid OnTextChanged event firing after spell checking highlights chat text. local RightClickedWord = nil --The current word under the CursorPosition that was right-clicked local RightClickedWordStartPos --Where that word starts local RightClickedWordEndPos --and where that word ends local RightClickedEditBox --and what EditBox was right clicked local OldLineLength --Tracks the previous length of the ChatEditBox.text local GuildRosterCalled = false local MaxColorCodes = 12 --The max amount of color codes we will add to the editbox text. local Misspelled_Saved_CTL_SendChatMessage local Misspelled_CTL_hookedversion=0 --Output debug messages to the DevTool addon (https://github.com/brittyazel/DevTool) function Misspelled:AddToInspector(data, strName) if DevTool and self.DEBUG then local valType = type(data) if valType == "string" then -- from: https://github.com/Gethe/wow-ui-source/blob/live/Interface/AddOns/Blizzard_SharedXML/Dump.lua -- %q "quotes" the string and escapes any special characters within it (like double quotes, newlines, etc.) -- replacing | with || outputs UI escaped sequence strings displaying all color and link details data = string_gsub(string_format("%q", data), "[|]", "||") DevTool:AddData(data, "Misspelled: " .. strName) else DevTool:AddData(data, "Misspelled: " .. strName) end end end function Misspelled:OnInitialize() --Enable to output debug messages created with calls to: AddToInspector(data, strName), to the addon: DevTool --self.DEBUG = true if Misspelled_DB == nil then Misspelled_DB = {} end --Check if the user has chosen to override the auto dictionary selection if Misspelled_DB.LoadDictionary == nil then Misspelled_DB.LoadDictionary = "enUS" end if Misspelled_DB.AutoSelectDictionary == nil then Misspelled_DB.AutoSelectDictionary = true end local dict = "Auto" if Misspelled_DB.AutoSelectDictionary == true then dict = "Auto" else dict = Misspelled_DB.LoadDictionary or "Auto" end --Load the Dictionary local dictLoaded if dict == "Auto" then dictLoaded = WordDict:Init() else dictLoaded = WordDict:Init(dict) end Misspelled:print("Misspelled: " .. L["Dictionary Loaded"] .." - " .. dictLoaded) --Load user dict Misspelled:LoadUserDict() Misspelled:print("Misspelled: " .. L["User Dictionary Loaded"]) -- Build Interface Options window self:CreateInterfaceOptions() --Watch for other chat addons: Wim, to load and then integrate. Misspelled:RegisterEvent("ADDON_LOADED") --Guild members and Friends are valid words. --Wait for the GUILD_ROSTER_UPDATE event to load --the guild members & friends into the dictionary as valid words. --Register the GUILD_ROSTER_UPDATE event, so we know when we can load the --Guild member names into the database Misspelled:RegisterEvent("GUILD_ROSTER_UPDATE") --GuildRoster can only be called every so often. --If another addon triggered it first, we might miss notification. --Moved to OnTextChanged --GuildRoster() --Patch 3.5 has multiple ChatEditBoxes. We need to hook in differently. --Updated for WoW 11.2.7: ChatEdit_ActivateChat exists but is never called. --Force the new timer-based approach. -- Use timer-based approach for 11.2.7+ local self = Misspelled local timer = CreateFrame("Frame") local elapsed = 0 timer:SetScript("OnUpdate", function(frame, delta) elapsed = elapsed + delta if elapsed < 0.1 then return end frame:SetScript("OnUpdate", nil) local n = _G.NUM_CHAT_WINDOWS or 10 for i = 1, n do local editbox = _G["ChatFrame" .. i .. "EditBox"] if editbox then local hooked = self:IsHooked(editbox, "OnTextChanged") if not hooked then self:WireUpEditBox(editbox) end end end end) if ChatEdit_ActivateChat ~= nil then Misspelled:SecureHook("ChatEdit_ActivateChat") elseif ChatFrameEditBox ~= nil then Misspelled:WireUpEditBox(ChatFrameEditBox) end -- hooks for removing any misspelled word highlighting in the text before the chat message is sent -- The Wow client will disconnect if you attempt to send a color tags in a chat message. Misspelled:RawHook("SendChatMessage", Misspelled.SendChatMessage, true) end function Misspelled:WireUpEditBox(editbox) Misspelled:SecureHookScript(editbox, "OnEscapePressed", Misspelled.EditBox_OnEscapePressed) Misspelled:SecureHookScript(editbox, "OnEnterPressed", Misspelled.EditBox_OnEnterPressed) Misspelled:SecureHookScript(editbox, "OnTextChanged", Misspelled.EditBox_OnTextChanged) Misspelled:HookScript(editbox, "OnMouseUp", Misspelled.EditBox_OnMouseUp) -- Used to hook mouse right-clicks to show suggestions frame end function Misspelled:GUILD_ROSTER_UPDATE() Misspelled:LoadGuildAndFriendRoster() end function Misspelled:ADDON_LOADED(event, addonName) if event == "ADDON_LOADED" and addonName == "WIM" and WIM then WIM.RegisterWidgetTrigger("msg_box", "whisper,chat,w2w", "OnEscapePressed", Misspelled.EditBox_OnEscapePressed) WIM.RegisterWidgetTrigger("msg_box", "whisper,chat,w2w", "OnEnterPressed", Misspelled.EditBox_OnEnterPressed) WIM.RegisterWidgetTrigger("msg_box", "whisper,chat,w2w", "OnTextChanged", Misspelled.EditBox_OnTextChanged) --WIM.RegisterWidgetTrigger("msg_box", "whisper,chat,w2w", "OnMouseUp", Misspelled.EditBox_OnMouseUp) --Before a chat message is sent, remove any highlighting that Misspelled might have added. --The Wow client will disconnect if you attempt to send a colored chat message. --If available use the WIM API: Wim.RegisterPreSendFilterText --WIM sends its chat messages via the API ChatThrottleLib, --ChatThrottleLib hooks the default SendChatMessage api, many times, before Misspelled can. --ChatThrottleLib is used in many addons, that potentially load before Misspelled. --So we have to hook ChatThrottleLib just to be safe. if(WIM.RegisterPreSendFilterText) then -- avoid error if WIM not up to date. WIM.RegisterPreSendFilterText(function(text) return Misspelled:RemoveHighlighting(text) end) else if(ChatThrottleLib and Misspelled_CTL_hookedversion < ChatThrottleLib.version) then Misspelled_Saved_CTL_SendChatMessage=ChatThrottleLib.SendChatMessage function ChatThrottleLib:SendChatMessage(prio, prefix, text, ...) text = Misspelled:RemoveHighlighting(text) --print("Misspelled Hooked ChatThrottleLib_SendChatMessaged called") return Misspelled_Saved_CTL_SendChatMessage(ChatThrottleLib, prio, prefix, text, ...) end Misspelled_CTL_hookedversion=ChatThrottleLib.version end end end end --Patch 3.5 Hook ChatFrame.lua ChatEdit_ActivateChat(editBox) function Misspelled:ChatEdit_ActivateChat(editBox) --Make sure this editbox is hooked --print("EditBox to hook: " .. editBox:GetName()) local hooked, hookHandler = Misspelled:IsHooked(editBox, "OnTextChanged") if hooked == false then Misspelled:WireUpEditBox(editBox) end self.hooks["ChatEdit_ActivateChat"](editBox) end --Before a chat message is sent, remove any highlighting that Misspelled might have added. --The Wow client will disconnect if you attempt to send Hex code colored text in a chat message. function Misspelled.SendChatMessage(message, chatType, languageID, target, ...) local cleanedMessage = Misspelled:RemoveHighlighting(message) --On DEBUG only --Misspelled:AddToInspector(cleanedMessage, "Misspelled:SendChatMessage - gotMessage") --self.hooks[C_ChatInfo]["SendChatMessage"](cleanedMessage, chatType, languageID, target, ...) local gameType = "Unknown" Misspelled.hooks["SendChatMessage"](cleanedMessage, chatType, languageID, target, ...) end --Possible changes: --A single new character was inserted at the end of the line (line length grew by 1) -- If this char is a word separator, then spell check the line -- --Any other change in the line, requires we recheck the entire line. --More than a single character was pasted or linked into the line. -- remove any highlighting and recheck the entire line. -- --A single character was removed from the line function Misspelled.EditBox_OnTextChanged(editbox) if SkipOnTextChanged == true then SkipOnTextChanged = false return end --print("Editbox name:", editbox:GetName()) --Load the guild roster if needed if GuildRosterCalled == false then if IsInGuild() then GuildRoster() --Request an updated guild roster from the server. else Misspelled:LoadGuildAndFriendRoster() end GuildRosterCalled = true end local text = editbox:GetText() local pos = editbox:GetCursorPosition() --print ("TextChaged:", editbox:GetCursorPosition(), string.gsub(editbox:GetText(), "\124", "\124\124")) local newLineLength = #text --Check if we should clear the WordCache table to save memory, if it's gotten very large if newLineLength == 0 then if WordCacheCount > WordCacheCountMax then WordCache = {} WordCacheCount = 0 end end --if the first char is a /, indicating some slash command, skip spellchecking the text. if (string_sub(text, 1, 1) == "/" ) then local cleanedChatMessage = Misspelled:RemoveHighlighting(text) if text ~= cleanedChatMessage then editbox:SetText(cleanedChatMessage) if pos == 1 or pos== 0 then editbox:SetCursorPosition(pos) end OldLineLength = #text end RightClickedWord = nil WordLocations[editbox:GetName()] = {} return end --If we currently just added one new char to the end of the line, see if it's a word boundary char --CursorPosition (pos) must be at the end of the line, and the line size must have had grown by --one character. if pos == #text and newLineLength -1 == OldLineLength then local lastChar = string_sub(text, -1, -1) if string_match(lastChar, "[ %(%);,%.!%?:\"]") ~= nil then Misspelled:SpellCheckChat(editbox) end elseif pos ~= #text then ----We must be making some other kind of edit someplace other than at the end of the line. ----Recheck the entire line Misspelled:SpellCheckChat(editbox) end --Save the new line length for use with the next round of OnTextChanged processing. OldLineLength = #editbox:GetText() end function Misspelled.EditBox_OnEscapePressed(editbox) RightClickedWord = nil WordLocations[editbox:GetName()] = {} end function Misspelled.EditBox_OnEnterPressed(editbox) RightClickedWord = nil WordLocations[editbox:GetName()] = {} --before message is sent, remove and misspelled highlighting local cleanedChatMessage = Misspelled:RemoveHighlighting(editbox:GetText()) WordLocations[editbox:GetName()] = {} editbox:SetText(cleanedChatMessage) end --Return a string where misspelled words are highlighted --If this string is a slash command "/", don't perform any highlighting --We should keep track of the current edit cursor position, --so we can report it's new position after highlighting. function Misspelled:SpellCheckChat(editbox) local editboxText = editbox:GetText() if #editboxText < 2 then return end --Ensure we have hooked the MouseUp event --Should fix Chatter changing the OnMouseUp script to nil. Bad Chatter local hooked, hookHandler = Misspelled:IsHooked(editbox, "OnMouseUp") if hooked == false or (hooked == true and hookHandler ~= self.OnMouseUp) then Misspelled:Unhook(editbox, "OnMouseUp") Misspelled:RawHookScript(editbox, "OnMouseUp", Misspelled.EditBox_OnMouseUp) end local newText = editboxText --Watch how many characters we insert or remove from the left side of the cursor position. --We'll adjust the cursor position to compensate for any misspelled word Hex code colored text added by Misspelled. local newCPos --remove any previous misspelling highlighting before checking the editboxText for misspellings. newText, newCPos = Misspelled:RemoveHighlighting(editboxText, editbox:GetCursorPosition()) WordLocations[editbox:GetName()] = {} --If this is a command, don't spellcheck or highlight if string_sub(editboxText, 1, 1) == "/" then if newText ~= editboxText then editbox:SetText(newText) editbox:SetCursorPosition(newCPos) end end --Find misspelled words & populate the WordLocations info. Misspelled:CheckLine(newText, editbox) local colorCodesAdded = 0 --Use the WordLocation info to march backwards through the input text, --highlighting misspellings --tprint(WordLocations) local w for x = #WordLocations[editbox:GetName()], 1, -1 do w = WordLocations[editbox:GetName()][x] if WordCache[w.Word].Correct == false then --Insert highlighting newText = string_sub(newText, 1, w.StartPos -1) .. SPELLED_WRONG_HIGHLIGHT .. string_sub(newText, w.StartPos, w.EndPos) .. FONT_COLOR_CODE_CLOSE .. string_sub(newText, w.EndPos + 1) --Adjust cursor position if the cursor was to the right of the first char in the word we're highlighting. if newCPos >= w.EndPos then newCPos = newCPos + #SPELLED_WRONG_HIGHLIGHT + #FONT_COLOR_CODE_CLOSE elseif newCPos >= w.StartPos then newCPos = newCPos + #SPELLED_WRONG_HIGHLIGHT end colorCodesAdded = colorCodesAdded + 1 if colorCodesAdded >= MaxColorCodes then --Truncate WordLocations to here, because there are too many misspelled words. --The WoW chatbox won't let us add more than so many color coded sections. --Delete all entries before x WordLocations[editbox:GetName()] = { unpack( WordLocations[editbox:GetName()], x ) } break end end end --Adjust the word's WordLocation StartPos and EndPos, wherever we added highlighting. --The right click handler uses this position info. to detect the misspelled word that was right clicked. --March forward this time local n = 0 --NewCharsAddedCounter for x = 1, #WordLocations[editbox:GetName()] do w = WordLocations[editbox:GetName()][x] if WordCache[w.Word].Correct == false then w.StartPos = w.StartPos + n + #SPELLED_WRONG_HIGHLIGHT n = n + #SPELLED_WRONG_HIGHLIGHT w.EndPos = w.EndPos + n n = n + #FONT_COLOR_CODE_CLOSE end end if newText ~= editboxText then --When we call settext, an OnSetText event will fire. --Execution of this event should be skipped to avoid SpellCheckChat from running twice. --Use a local toggle to skip this second firing SkipOnTextChanged = true editbox:SetText(newText) editbox:SetCursorPosition(newCPos) end end --Finds all words in the input text string, ignoring any Wow hyperlinks or textures. --These links or textures will be replaced with a sequence of # characters, the same length of the color+link, --so the contents of these are not spellchecked. -- --Next populate the table, WordLocations, storing the following info. on each word: -- WordLocations[x] = {["word"] = word, ["StartPos"] = matchPosStart, ["EndPos"] = matchPosEnd} -- x == Ordinal position of the word in the text string -- --Next check each word to see we need to cache spell check info for the word. --Words with numbers or words in all upper case are ignored. --Store in the WordCache table whether the word is spell correctly, along with the --any misspelled replacement suggestions. -- --Trim the WordCache table if it's grown very large function Misspelled:CheckLine(text, editbox) --Reset the info on where each word is located WordLocations[editbox:GetName()] = {} if text == nil then return end if #text == 0 then return end --Find if there are any WoW UI escape sequences on this line, and replace them with # chars, --so they don't match as words in the next stage of parsing and get ignored for spellchecking. --ref: https://warcraft.wiki.gg/wiki/UI_escape_sequences -- |cn[^:]+:.*|r -- Global Colors text (new in 11.1.5 - |cncolorname:text|r) -- |cnIQ%d:.*|r -- Item Quality Colors (new in 11.1.5 - |cnIQn:text|r -- |[Cc]%x-|H.+|h.+|h|r -- Hex color coded text with links (format: |cffxxxxxx|Htype:payload|h[text]|h|r ) https://warcraft.wiki.gg/wiki/Hyperlinks -- |cn[^:]+:|H.+|h.+h|r -- Global Colors / Custom item color links (new in 11.1.5 - |cncolorname:text|r) https://warcraft.wiki.gg/wiki/UI_escape_sequences#:~:text=back%20to%20white-,Global%20Colors,-%7Ccncolorname%3A -- |H.*|h -- Links -- |T.*|t -- Textures -- |A.*|a -- Texture Atlas -- {.-} -- Raid target icons -- |n -- newline character local newText = text -- newText = string_gsub(newText, "(|[Cc]%x-|H.-|h.-|h|r)", function(x) return string_rep("#", #x) end) -- newText = string_gsub(newText, "(|H.*|h)", function(x) return string_rep("#", #x) end) -- newText = string_gsub(newText, "(|T.*|t)", function(x) return string_rep("#", #x) end) -- newText = string_gsub(newText, "({.-})", function(x) return string_rep("#", #x) end) -- newText = string_gsub(newText, "(|n)", function(x) return string_rep("#", #x) end) local WowTextMarkupEscapes = { ["(|cn[^:]+:.-|r)"] = "#", -- Global Colors text ["(|cnIQ%d:.-|r)"] = "#", -- Item Quality Colors text ["(|[Cc]%x-|H.-|h.-|h|r)"] = "#", -- Hex color coded text with optional colored links ["(|H.-|h)"] = "#", -- Links ["(|T.-|t)"] = "#", -- Textures ["(|A.-|a)"] = "#", -- Texture Atlas ["({.-})"] = "#", -- Raid target icons ["(|n)"] = "#" -- Newline character } for k, v in pairs(WowTextMarkupEscapes) do newText = string_gsub(newText, k, function(x) return string_rep(v, #x) end) end --March through the text, finding the words, record there start & end positions, and spell check status local patt = "[A-Za-z0-9_'À-ÿœæŒÆ]+" local matchPosStart local matchPosEnd matchPosStart, matchPosEnd = string_find(newText, patt) local x = 0 local word local correct while matchPosStart ~= nil do word = string_sub(newText, matchPosStart, matchPosEnd) --ignore all uppercase words if word ~= string_upper(word) then --ignore words with numbers in them if string_match(word, "[%d]") == nil then x = x + 1 WordLocations[editbox:GetName()][x] = {["Word"] = word, ["StartPos"] = matchPosStart, ["EndPos"] = matchPosEnd} if WordCache[word] == nil then correct = false --Ignore words in all upper case if word == string_upper(word) then correct = true end --See if the dictionary contains the word if correct == false then correct = WordDict:Contains(word) end --Try the lower case version of the word if correct == false then correct = WordDict:Contains(string_lower(word)) end --Cache the results WordCache[word] = {["Correct"] = correct} --, ["Suggestions"] = {}} WordCacheCount = WordCacheCount + 1 --Changed to delay searching for suggestions until someone right-clicks on a misspelled word. --Adding UTF8 support slows the suggestion generation. --~ if correct == false then --~ local suggestions = {} --~ suggestions = WordDict:Suggest(word) --~ if #suggestions > 0 then --~ WordCache[word].Suggestions = suggestions --~ end --~ end end end end matchPosStart, matchPosEnd = string_find(newText, patt, matchPosEnd+1) end end --Return a string where the highlighting has been removed from any misspelled words, --with the goal to not change or destroy any other UI escaped sequences present in the chat message, --such as colored text, Wow itemLinks or textures. --(https://warcraft.wiki.gg/wiki/UI_escape_sequences & http://www.wowwiki.com/ItemLink) -- --Misspelled word highlighting is added with a Hex color coded UI escape sequence: |cff7dc6fbMisspelledtext|r --The chat message text may contain other UI escaped sequences such at item links. --The chat message text could also contain one or more pipe characters ||, complicating parsing. -- -- Note: Lua's standard regular expression library has limitations compared to some other regex engines. -- It does not support features like negative lookahead assertions ((?!...)), -- which are typically used to assert that a sequence is not present. -- Therefore, a single Lua regular expression cannot directly say "match everything until |r, but fail if |h is encountered before that". -- --Potential refactor: -- 1) Use a regular expression to match any block starting with |cff7dc6fb and ending with |r, capturing everything in between. -- regex capture: |c%x-(.-)|r -- 2) Check the captures text to ensure it does not contain the sequence |h. -- --It's possible that a recent edit has started to destroy the color tags, either at the --beginning or end of a highlighted misspelled word. --Attempt to detect this and remove any dangling colored text tags. function Misspelled:RemoveHighlighting(text, ...) -- \124 is the ASCII code for the pipe '|' character. --Misspelled:AddToInspector(string_gsub(text, "\124", "\124\124"), "RemoveHighlighting-input") --Blizzard uses string.gsub(textString, "[|]", "||"), in the /dump source code local cleanedChatMessage local newText = text --Track the number of characters removed from the left side of the current cursor position. local cPos if ... ~= nil then cPos = ... else cPos = 0 end local itemLinks = {} local itemLink local itemLinkNum = 1 local patt, matchPosStart, matchPosEnd local tokenSize local tempToken local tempText local charsRemoved --Tracks the chars removed from the left of the cursor --Try and match Global Colors text. (|cncolorname:text|r) --Use a non-greedy match character (-) in the match pattern rather than a greed match character (*) patt = "(|cn[^:]+:.-|r)" matchPosStart, matchPosEnd = string_find(newText, patt) while matchPosStart ~= nil do --Store the itemlink and it's relative position so it can be replaced latter itemLink = string_sub(newText, matchPosStart, matchPosEnd) itemLinks[itemLinkNum] = itemLink tokenSize = #itemLink tempToken = "{<<" .. tostring(itemLinkNum) tempToken = tempToken .. string_rep(">", tokenSize - #tempToken - 1) .. "}" --Replace this itemlink with a temporary placeholder code newText = string_gsub(newText, patt, tempToken, 1) itemLinkNum = itemLinkNum + 1 matchPosStart, matchPosEnd = string_find(newText, patt) end --Try and match (IQn) Item Quality Colors text. (|cnIQn:text|r) --Use a non-greedy match character (-) in the match pattern rather than a greed match character (*) patt = "(|cnIQ%d:.-|r)" matchPosStart, matchPosEnd = string_find(newText, patt) while matchPosStart ~= nil do --Store the itemlink and it's relative position so it can be replaced latter itemLink = string_sub(newText, matchPosStart, matchPosEnd) itemLinks[itemLinkNum] = itemLink tokenSize = #itemLink tempToken = "{<<" .. tostring(itemLinkNum) tempToken = tempToken .. string_rep(">", tokenSize - #tempToken - 1) .. "}" --Replace this itemlink with a temporary placeholder code newText = string_gsub(newText, patt, tempToken, 1) itemLinkNum = itemLinkNum + 1 matchPosStart, matchPosEnd = string_find(newText, patt) end --Try and match Hex code colored Item links. The Addon GHI (Gryphonheart Items) colors links with a capitol C, non-standard. patt = "|[Cc]%x+|H.-|h.-|h|r" matchPosStart, matchPosEnd = string_find(newText, patt) while matchPosStart ~= nil do --Store the itemlink and it's relative position so it can be replaced latter itemLink = string_sub(newText, matchPosStart, matchPosEnd) itemLinks[itemLinkNum] = itemLink tokenSize = #itemLink tempToken = "{<<" .. tostring(itemLinkNum) tempToken = tempToken .. string_rep(">", tokenSize - #tempToken - 1) .. "}" --Replace this itemlink with a temporary placeholder code newText = string_gsub(newText, patt, tempToken, 1) itemLinkNum = itemLinkNum + 1 matchPosStart, matchPosEnd = string_find(newText, patt) end --Try to match, non-colored Item Links patt = "|H.-|h" matchPosStart, matchPosEnd = string_find(newText, patt) while matchPosStart ~= nil do --Store the itemlink and it's relative position so it can be replaced latter itemLink = string_sub(newText, matchPosStart, matchPosEnd) itemLinks[itemLinkNum] = itemLink tokenSize = #itemLink tempToken = "{<<" .. tostring(itemLinkNum) tempToken = tempToken .. string_rep(">", tokenSize - #tempToken - 1) .. "}" --Replace this itemlink with a temporary placeholder code newText = string_gsub(newText, patt, tempToken, 1) itemLinkNum = itemLinkNum + 1 matchPosStart, matchPosEnd = string_find(newText, patt) end --Try to match and textures links. (i.e. Raid targets and there used when chatting with a GM) patt = "|T.-|t" matchPosStart, matchPosEnd = string_find(newText, patt) while matchPosStart ~= nil do --Store the itemlink and it's relative position so it can be replaced latter itemLink = string_sub(newText, matchPosStart, matchPosEnd) itemLinks[itemLinkNum] = itemLink tokenSize = #itemLink tempToken = "{<<" .. tostring(itemLinkNum) tempToken = tempToken .. string_rep(">", tokenSize - #tempToken - 1) .. "}" --Replace this itemlink with a temporary placeholder code newText = string_gsub(newText, patt, tempToken, 1) itemLinkNum = itemLinkNum + 1 matchPosStart, matchPosEnd = string_find(newText, patt) end --Remove the highlighting from any misspelled words. --i.e. When the beginning SPELLED_WRONG_HIGHLIGHT Hex coded color tag and ending |r tag wrap text. --Adjust the cursor position as needed. patt = SPELLED_WRONG_HIGHLIGHT .. "(.-)|r" matchPosStart, matchPosEnd = string_find(newText, patt) Misspelled:AddToInspector({_patt=string_gsub(patt,"[|]","||"),_newText=string_gsub(newText,"[|]","||"),_matchPosStart=matchPosStart,_matchPosEnd=matchPosEnd},"RemoveHighlighting string.find misspelled highlighting") while matchPosStart ~= nil do --strings.gsub(input, pattern, replaceText, n=limit the number of substations to be made) tempText = string_gsub(newText, patt, "%1", 1) --tempText = string_gsub(newText, patt, function(x) return x end, 1) if #newText - #tempText ~= 0 then charsRemoved = 0 --If the cursor was to the right of the start, subtract the num of deleted chars from the cursor position if cPos >= matchPosStart then charsRemoved = (#newText - #tempText) end --If the cursor was to the left of the end color tag and to the right of the start, add 2 if cPos >= matchPosStart and cPos < matchPosEnd then charsRemoved = charsRemoved - 2 end cPos = cPos - charsRemoved end newText = tempText matchPosStart, matchPosEnd = string_find(newText, patt) end --Remove any remaining orphaned beginning color tags. patt = "|[Cc]%x%x%x%x%x%x%x%x" matchPosStart, matchPosEnd = string_find(newText, patt) while matchPosStart ~= nil do tempText = string_gsub(newText, patt, "", 1) if #newText - #tempText ~= 0 then --If the cursor was to the right of the start, subtract the num of deleted chars from the cursor position if cPos >= matchPosStart then cPos = cPos - (#newText - #tempText) end end newText = tempText matchPosStart, matchPosEnd = string_find(newText, patt) end --Remove any remaining orphaned ending color tags. patt = "|r" matchPosStart, matchPosEnd = string_find(newText, patt) while matchPosStart ~= nil do tempText = string_gsub(newText, patt, "", 1) if #newText - #tempText ~= 0 then --If the cursor was to the right of the start, subtract the num of deleted chars from the cursor position if cPos >= matchPosStart then cPos = cPos - (#newText - #tempText) end end newText = tempText matchPosStart, matchPosEnd = string_find(newText, patt) end --Replace back the escape sequences extracted if #itemLinks > 0 then for i,val in ipairs(itemLinks) do newText = string_gsub(newText, "{<<" .. tostring(i) .. ">-}", val) end end --If by chance the tracked cursor position went negative, set it to 0 if cPos < 0 then cPos = 0 end cleanedChatMessage = newText --cPos should never be > #newText, unless there's some unfound error above return cleanedChatMessage, cPos end function Misspelled:TestRemoveHighlighting() local testMessage local checkMessage local cleanedMessage local newCPos local testResult --Test 1 testID = "1" testMessage = "Apple" checkMessage= "Apple" cleanedMessage, newCPos = Misspelled:RemoveHighlighting(testMessage, string_len(testMessage)) local testResults_table = { _testMessage = string_gsub(testMessage,"[|]","||"), _checkMessage = string_gsub(checkMessage,"[|]","||"), _cleanedMessage = string_gsub(cleanedMessage,"[|]","||"), _CPos = string_len(testMessage), _newCPos = newCPos } if cleanedMessage == checkMessage then testResult = "passed" else testResult = "failed" end Misspelled:AddToInspector(testResults_table,"Test ".. testResult .. ": RemoveHighlighting "..testID) --Test2 - Misspelled highlighted word: Applez testID = "2" testMessage = "|cff7dc6fbApplez|r" checkMessage= "Applez" cleanedMessage, newCPos = Misspelled:RemoveHighlighting(testMessage, string_len(testMessage)) local testResults_table = { _testMessage = string_gsub(testMessage,"[|]","||"), _checkMessage = string_gsub(checkMessage,"[|]","||"), _cleanedMessage = string_gsub(cleanedMessage,"[|]","||"), _CPos = string_len(testMessage), _newCPos = newCPos } if cleanedMessage == checkMessage then testResult = "passed" else testResult = "failed" end Misspelled:AddToInspector(testResults_table,"Test ".. testResult .. ": RemoveHighlighting "..testID) --Test3 - Misspelled highlighted word: Applez good. testID = "3" testMessage = "|cff7dc6fbApplez|r good." checkMessage= "Applez good." cleanedMessage, newCPos = Misspelled:RemoveHighlighting(testMessage, string_len(testMessage)) local testResults_table = { _testMessage = string_gsub(testMessage,"[|]","||"), _checkMessage = string_gsub(checkMessage,"[|]","||"), _cleanedMessage = string_gsub(cleanedMessage,"[|]","||"), _CPos = string_len(testMessage), _newCPos = newCPos } if cleanedMessage == checkMessage then testResult = "passed" else testResult = "failed" end Misspelled:AddToInspector(testResults_table,"Test ".. testResult .. ": RemoveHighlighting "..testID) --Test4 - Correctly spelled word [Link] correctly spelled word testID = "4" testMessage = "Test |cff71d5ff|Hspell:2061:0|h[Flash Heal]|h|r good." checkMessage= "Test |cff71d5ff|Hspell:2061:0|h[Flash Heal]|h|r good." cleanedMessage, newCPos = Misspelled:RemoveHighlighting(testMessage, string_len(testMessage)) local testResults_table = { _testMessage = string_gsub(testMessage,"[|]","||"), _checkMessage = string_gsub(checkMessage,"[|]","||"), _cleanedMessage = string_gsub(cleanedMessage,"[|]","||"), _CPos = string_len(testMessage), _newCPos = newCPos } if cleanedMessage == checkMessage then testResult = "passed" else testResult = "failed" end Misspelled:AddToInspector(testResults_table,"Test ".. testResult .. ": RemoveHighlighting "..testID) --Test5 - Correctly spelled word [Spell Link] incorrectly spelled word testID = "5" testMessage = "Test |cff71d5ff|Hspell:2061:0|h[Flash Heal]|h|r |cff7dc6fbbadd|r." checkMessage= "Test |cff71d5ff|Hspell:2061:0|h[Flash Heal]|h|r badd." cleanedMessage, newCPos = Misspelled:RemoveHighlighting(testMessage, string_len(testMessage)) local testResults_table = { _testMessage = string_gsub(testMessage,"[|]","||"), _checkMessage = string_gsub(checkMessage,"[|]","||"), _cleanedMessage = string_gsub(cleanedMessage,"[|]","||"), _CPos = string_len(testMessage), _newCPos = newCPos } if cleanedMessage == checkMessage then testResult = "passed" else testResult = "failed" end Misspelled:AddToInspector(testResults_table,"Test ".. testResult .. ": RemoveHighlighting "..testID) --Test6 - Correctly spelled word [Item link] testID = "6" testMessage = "Off-hand: |cffa335ee|Hitem:222566::::::::80:258::13:1:3524:6:40:2249:38:8:45:211296:46:226024:47:222584:48:224072:::::|h[Vagabond's Torch |A:Professions-ChatIcon-Quality-Tier5:17:17::1|a]|h|r" checkMessage= "Off-hand: |cffa335ee|Hitem:222566::::::::80:258::13:1:3524:6:40:2249:38:8:45:211296:46:226024:47:222584:48:224072:::::|h[Vagabond's Torch |A:Professions-ChatIcon-Quality-Tier5:17:17::1|a]|h|r" cleanedMessage, newCPos = Misspelled:RemoveHighlighting(testMessage, string_len(testMessage)) local testResults_table = { _testMessage = string_gsub(testMessage,"[|]","||"), _checkMessage = string_gsub(checkMessage,"[|]","||"), _cleanedMessage = string_gsub(cleanedMessage,"[|]","||"), _CPos = string_len(testMessage), _newCPos = newCPos } if cleanedMessage == checkMessage then testResult = "passed" else testResult = "failed" end Misspelled:AddToInspector(testResults_table,"Test ".. testResult .. ": RemoveHighlighting "..testID) --Test7 - Correctly spelled word [Hex colored Item link] incorrectly spelled word. testID = "7" testMessage = "Off-hand: |cffa335ee|Hitem:222566::::::::80:258::13:1:3524:6:40:2249:38:8:45:211296:46:226024:47:222584:48:224072:::::|h[Vagabond's Torch |A:Professions-ChatIcon-Quality-Tier5:17:17::1|a]|h|r |cff7dc6fbbadd|r." checkMessage= "Off-hand: |cffa335ee|Hitem:222566::::::::80:258::13:1:3524:6:40:2249:38:8:45:211296:46:226024:47:222584:48:224072:::::|h[Vagabond's Torch |A:Professions-ChatIcon-Quality-Tier5:17:17::1|a]|h|r badd." cleanedMessage, newCPos = Misspelled:RemoveHighlighting(testMessage, string_len(testMessage)) local testResults_table = { _testMessage = string_gsub(testMessage,"[|]","||"), _checkMessage = string_gsub(checkMessage,"[|]","||"), _cleanedMessage = string_gsub(cleanedMessage,"[|]","||"), _CPos = string_len(testMessage), _newCPos = newCPos } if cleanedMessage == checkMessage then testResult = "passed" else testResult = "failed" end Misspelled:AddToInspector(testResults_table,"Test ".. testResult .. ": RemoveHighlighting "..testID) --Test8 - Correctly spelled word [cnIQ#: colored Item link] testID = "8" testMessage = "test: |cnIQ2:|Hitem:225566::::::::80:258:::::::::|h[Warped Wing]|h|r" checkMessage = "test: |cnIQ2:|Hitem:225566::::::::80:258:::::::::|h[Warped Wing]|h|r" cleanedMessage, newCPos = Misspelled:RemoveHighlighting(testMessage, string_len(testMessage)) local testResults_table = { _testMessage = string_gsub(testMessage,"[|]","||"), _checkMessage = string_gsub(checkMessage,"[|]","||"), _cleanedMessage = string_gsub(cleanedMessage,"[|]","||"), _CPos = string_len(testMessage), _newCPos = newCPos } if cleanedMessage == checkMessage then testResult = "passed" else testResult = "failed" end Misspelled:AddToInspector(testResults_table,"Test ".. testResult .. ": RemoveHighlighting "..testID) --Test9 - Correctly spelled word [cnIQ#: colored Item link] incorrectly spelled word. testID = "9" testMessage = "test: |cnIQ2:|Hitem:225566::::::::80:258:::::::::|h[Warped Wing]|h|r |cff7dc6fbbadd|r." checkMessage = "test: |cnIQ2:|Hitem:225566::::::::80:258:::::::::|h[Warped Wing]|h|r badd." cleanedMessage, newCPos = Misspelled:RemoveHighlighting(testMessage, string_len(testMessage)) local testResults_table = { _testMessage = string_gsub(testMessage,"[|]","||"), _checkMessage = string_gsub(checkMessage,"[|]","||"), _cleanedMessage = string_gsub(cleanedMessage,"[|]","||"), _CPos = string_len(testMessage), _newCPos = newCPos } if cleanedMessage == checkMessage then testResult = "passed" else testResult = "failed" end Misspelled:AddToInspector(testResults_table,"Test ".. testResult .. ": RemoveHighlighting "..testID) end ------------------------------------------------------------------------- -- -- Routines for the right click misspelled suggestions popup. -- ------------------------------------------------------------------------- function Misspelled:OnMouseUp(editbox, button) if button == "RightButton" then local badWordFound = false CloseDropDownMenus() --check if we are positioned on a misspelled word local pos = editbox:GetCursorPosition() if WordLocations[editbox:GetName()] ~= nil then for i, w in ipairs(WordLocations[editbox:GetName()]) do if pos >= w.StartPos and pos <= w.EndPos then if WordCache[w.Word].Correct == false then --If not cached, lookup Suggestions for the misspelled word if WordCache[w.Word].Suggestions == nil then WordCache[w.Word].Suggestions = WordDict:Suggest(w.Word) end RightClickedWord = w.Word RightClickedWordStartPos = w.StartPos RightClickedWordEndPos = w.EndPos RightClickedEditBox = editbox badWordFound = true ToggleDropDownMenu(1, nil, MisspelledSuggestions_DropDown, "cursor") else RightClickedWord = nil end break end end end --Show where the user right clicked. --print("cursor position:" , ChatFrameEditBox:GetCursorPosition()) --If we are not positioned on a bad, word call and MouseUp handler that was hooked to the box. if badWordFound == false then --print("Trigger native OnMouseUp call") self.hooks[editbox]["OnMouseUp"](editbox, button) end end end function MisspelledSuggestions_InitializeDropDown(level) if RightClickedWord == nil then return end if #RightClickedWord == 0 then return end do local info = UIDropDownMenu_CreateInfo() info.text = L["Suggestions for:"] .. " " .. RightClickedWord info.isTitle = 1 info.notClickable = 1 info.notCheckable = true UIDropDownMenu_AddButton(info) end --Add suggestions to the DropDown for i, s in ipairs(WordCache[RightClickedWord].Suggestions) do do local info = UIDropDownMenu_CreateInfo() --Line below causes a this == nil error in 4.0. Looks like it's not needed. --info.owner = this:GetParent() --If the misspelled word's first info.text = s.Word --If this suggestion is either a guild member or friend append a note. if Misspelled:IsGuildMember(s.Word) == true then info.text = info.text .. " " .. L["(Guild)"] else if Misspelled:IsFriend(s.Word) == true then info.text = info.text .. " " .. L["(Friend)"] end end info.isTitle = nil info.notCheckable = true info.value = s.Word info.func = function() SuggestionsFrame_Click(s.Word, RightClickedEditBox) end --Add the above info to the options menu as clickable item UIDropDownMenu_AddButton(info) end end do --Add a non-clickable separator local info = UIDropDownMenu_CreateInfo() --info.owner = this:GetParent() info.text = "" info.isTitle = nil info.value = "" info.notClickable = 1 info.notCheckable = true UIDropDownMenu_AddButton(info) end do local info = UIDropDownMenu_CreateInfo() --info.owner = this:GetParent() info.text = L["Ignore All"] info.isTitle = nil info.value = RightClickedWord info.func = function() SuggestionsFrame_Click("###IgnoreAll", RightClickedEditBox) end info.notClickable = nil info.notCheckable = true UIDropDownMenu_AddButton(info) end do local info = UIDropDownMenu_CreateInfo() --info.owner = this:GetParent() info.text = L["Add to Dictionary"] info.isTitle = nil info.value = RightClickedWord info.func = function() SuggestionsFrame_Click("###AddToDictionary", RightClickedEditBox) end info.notClickable = nil info.notCheckable = true UIDropDownMenu_AddButton(info) end do local info = UIDropDownMenu_CreateInfo() --info.owner = this:GetParent() info.text = L["Cancel"] info.isTitle = nil info.value = nil info.notClickable = nil info.notCheckable = true UIDropDownMenu_AddButton(info) end end function MisspelledSuggestions_DropDownOnLoad(self) UIDropDownMenu_Initialize(self, MisspelledSuggestions_InitializeDropDown, "MENU") end function SuggestionsFrame_Click(value, editbox) --value will equal the suggestion word clicked, --or a "special" tag, for the 'Ignore' and 'Add to Dictionary' functions --print("Suggestion Clicked: ", value) local newText = editbox:GetText() local newCursorPos = nil local isCapitalizedRightClickedWord = false --Check if the local (global) var RightClickedWord is populated with a non nil value assert(RightClickedWord ~= nil, "Misspelled: SuggestionsFrame_Click, Unexpected: RightClickedWord == nil") if string_sub(RightClickedWord, 1, 1) == string_upper(string_sub(RightClickedWord, 1, 1)) then isCapitalizedRightClickedWord = true end if value == "###IgnoreAll" then --Add this word to the WordCache so it will be ignored as misspelled until you reload --print("Ignore:", RightClickedWord) WordCache[RightClickedWord].Correct = true WordCache[RightClickedWord].Suggestions = {} --Remove the misspelled highlighting newText = string_sub(newText, 1, RightClickedWordStartPos - 1 - #SPELLED_WRONG_HIGHLIGHT) .. RightClickedWord .. string_sub(newText, RightClickedWordEndPos + #FONT_COLOR_CODE_CLOSE + 1) newCursorPos = RightClickedWordStartPos + #RightClickedWord - #SPELLED_WRONG_HIGHLIGHT - #FONT_COLOR_CODE_CLOSE + 1 elseif value == "###AddToDictionary" then -- Add this word to the user dictionary --print("Add to UserDict", RightClickedWord) Misspelled:AddToUserDict(RightClickedWord) WordCache[RightClickedWord].Correct = true WordCache[RightClickedWord].Suggestions = {} --Remove the misspelled highlighting newText = string_sub(newText, 1, RightClickedWordStartPos - 1 - #SPELLED_WRONG_HIGHLIGHT) .. RightClickedWord .. string_sub(newText, RightClickedWordEndPos + #FONT_COLOR_CODE_CLOSE + 1) newCursorPos = RightClickedWordStartPos + #RightClickedWord - #SPELLED_WRONG_HIGHLIGHT - #FONT_COLOR_CODE_CLOSE + 1 else --replace this word with the selected suggestion. --Remove the misspelled highlighting in the process. -- --If the misspelled word was capitalized, capitalize the replacement. if isCapitalizedRightClickedWord == true then value = string_upper(string_sub(value, 1, 1)) .. string_sub(value, 2) end newText = string_sub(newText, 1, RightClickedWordStartPos - 1 - #SPELLED_WRONG_HIGHLIGHT) .. value .. string_sub(newText, RightClickedWordEndPos + #FONT_COLOR_CODE_CLOSE + 1) --save the cursor position just after the corrected word, so we can set it later newCursorPos = RightClickedWordStartPos + #value - #SPELLED_WRONG_HIGHLIGHT - #FONT_COLOR_CODE_CLOSE + 1 --Note: if the replacement word is a different length, then the WordLocations will be messed up. end editbox:SetText(newText) --printable = gsub(newText, "\124", "\124\124") --\124 == "|" --print("New ChatText:", printable) --If we replaced a word, with a suggestion, move the cursor to the end of the new word. if newCursorPos ~= nil then --Check if the character to the right of the cursor is a space. If so adjust the cursor to the right by 1 char. if #newText > newCursorPos then if string_sub(newText, newCursorPos + 1, newCursorPos + 1) == " " then newCursorPos = newCursorPos + 1 end end editbox:SetCursorPosition(newCursorPos) end RightClickedWord = nil end ----------------------------------------------------- -- End: Right click Suggestions popup ----------------------------------------------------- ------------------------------------------------------------------------- -- -- Routines for dealing with the user dictionary -- ------------------------------------------------------------------------- --Load the words saved in the Users dictionary into the baseWords table. --In r18 we changed the in memory format used to store the baseWords, affixCode and PhoneticCode, --Saved a ton of memory not using a sub-table per baseWord. --If necessary convert the user dictionary storage to match the newer format. function Misspelled:LoadUserDict() if Misspelled_DB == nil then Misspelled_DB = {UserDict = {}} end local affixKeys, pCode if Misspelled_DB.UserDict ~= nil then for k, v in pairs(Misspelled_DB.UserDict) do --If needed convert the user dictionary format to the, post r18 format. if type(v) == "table" then affixKeys = v[1] pCode = v[2] if affixKeys == nil then affixKeys = "" end if pCode == nil then pCode = "" end v = affixKeys .. "/" .. pCode Misspelled_DB.UserDict[k] = v end if WordDict.baseWords[k] == nil then WordDict.baseWords[k] = v end end end end --Add a new word to the users dictionary, and the currently loaded baseWords table. --Store both the word and it's phonetic code. function Misspelled:AddToUserDict(word) if word == nil then return end if #word == 0 then return end if Misspelled_DB.UserDict == nil then Misspelled_DB.UserDict = {} end --Add the new word to the UserDict saved variable local pcode = WordDict:PhoneticCode(word) Misspelled_DB.UserDict[word] = "/" .. pcode --And add it to the currently loaded dictionary WordDict.baseWords[word] = "/" .. pcode --Fixup the WordCache WordCache[word].Correct = true WordCache[word].Suggestions = {} end local Misspelled_Words_To_Delete = {} function Misspelled:EditUserDict() if not Misspelled_DB then Misspelled_DB = {} end if not Misspelled_DB.UserDict then Misspelled_DB.UserDict = {} end Misspelled_Words_To_Delete = {} local f = _G.MisspelledUserDictFrame if not f then f = CreateFrame("Frame", "MisspelledUserDictFrame", UIParent) f:SetWidth(320); f:SetHeight(420); f:SetPoint("CENTER") f:SetBackdrop({bgFile="Interface\\DialogFrame\\UI-DialogBox-Background", edgeFile="Interface\\DialogFrame\\UI-DialogBox-Border", tile=true, tileSize=32, edgeSize=32, insets={left=11,right=12,top=12,bottom=11}}) local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") title:SetPoint("TOP", 0, -20); title:SetText(L["User Dictionary"]) local scroll = CreateFrame("ScrollFrame", "MisspelledUserDictScroll", f, "UIPanelScrollFrameTemplate") scroll:SetPoint("TOPLEFT", 24, -52); scroll:SetPoint("BOTTOMRIGHT", -42, 52) local content = CreateFrame("Frame", nil, scroll); content:SetWidth(245); scroll:SetScrollChild(content) f.content = content local del = CreateFrame("Button", nil, f, "UIPanelButtonTemplate") del:SetWidth(100); del:SetHeight(24); del:SetPoint("BOTTOM", 0, 18); del:SetText("Delete") del:SetScript("OnClick", function() for word in pairs(Misspelled_Words_To_Delete) do Misspelled_DB.UserDict[word] = nil; WordDict.baseWords[word] = nil end WordCache = {}; WordCacheCount = 0; f:Hide() end) f.deleteButton = del end for _, child in ipairs({f.content:GetChildren()}) do child:Hide(); child:SetParent(nil) end local row = 0 for word in pairs(Misspelled_DB.UserDict) do row = row + 1 local b = CreateFrame("Button", nil, f.content) b:SetHeight(18); b:SetWidth(240); b:SetPoint("TOPLEFT", 0, -(row-1)*20) b.text = b:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") b.text:SetPoint("LEFT"); b.text:SetText(word) b:SetScript("OnClick", function(self) if Misspelled_Words_To_Delete[word] then Misspelled_Words_To_Delete[word] = nil; self.text:SetTextColor(1,1,1) else Misspelled_Words_To_Delete[word] = true; self.text:SetTextColor(1,.2,0) end end) end f.content:SetHeight(math.max(1, row*20)); f.deleteButton:Enable(); f:Show() end ------------------------------------------------------------------------- -- End: Routines for dealing with the user dictionary ------------------------------------------------------------------------- ------------------------------------------------------------------------- -- -- Load Guild and Friend Roster Routine -- ------------------------------------------------------------------------- --Load the player's guild members and friends, as valid words into the --loaded dictionary. function Misspelled:LoadGuildAndFriendRoster() local numFriends, f local pcode --print("Misspelled: Friends names and guild members loading...") --First check your friends list if GetNumFriends then numFriends = GetNumFriends() if numFriends > 0 then for i = 1, numFriends do local friendName if C_FriendList and C_FriendList.GetFriendInfoByIndex then f = C_FriendList.GetFriendInfoByIndex(i) friendName = f and f.name else friendName = GetFriendInfo(i) end if friendName and #friendName > 0 then if WordDict:Contains(friendName) == false then if WordDict.soundslike == WordDict.Const.SoundslikeAlgorithms.PHONETIC then pcode = WordDict:PhoneticCode(friendName) elseif WordDict.soundslike == WordDict.Const.SoundslikeAlgorithms.GENERIC then pcode = WordDict:GenericSoundsLike(friendName) else pcode = "" end WordDict.baseWords[friendName] = "/" .. pcode end end end end end -- Guild members are valid words. local numTotalInGuild, guildMemberName if GetNumGuildMembers() ~= 0 then numTotalInGuild = GetNumGuildMembers() if ( numTotalInGuild > 0 ) then for i=1, numTotalInGuild do guildMemberName = GetGuildRosterInfo(i); if guildMemberName ~= nil then if #guildMemberName ~= 0 then if WordDict:Contains(guildMemberName) == false then --Look up the phonetic code for this guild member name if WordDict.soundslike == WordDict.Const.SoundslikeAlgorithms.PHONETIC then pcode = WordDict:PhoneticCode(guildMemberName) elseif WordDict.soundslike == WordDict.Const.SoundslikeAlgorithms.GENERIC then pcode = WordDict:GenericSoundsLike(guildMemberName) else pcode = "" end --Add the guild member to the loaded dictionary WordDict.baseWords[guildMemberName] = "/" .. pcode end end end end end --print("Misspelled: Guild Members Loaded") Misspelled:UnregisterEvent("GUILD_ROSTER_UPDATE") end end ------------------------------------------------------------------------- -- End: Load Guild and Friend Roster Routine ------------------------------------------------------------------------- --[[ Interface Options Window ]]-- function Misspelled:CreateInterfaceOptions() local cfgFrame = CreateFrame("FRAME", nil, UIParent) cfgFrame.name = "Misspelled" local cfgFrameHeader = cfgFrame:CreateFontString("OVERLAY", nil, "GameFontNormalLarge") cfgFrameHeader:SetPoint("TOPLEFT", 15, -15) cfgFrameHeader:SetText(self.Version) local cfgFrameReloadTip = cfgFrame:CreateFontString("OVERLAY", nil, "GameFontNormal") cfgFrameReloadTip:SetPoint("TOPLEFT", 20, -252) cfgFrameReloadTip:SetText(L["Note: reload the game UI to load a different selected dictionary"]) local cfgAutoSelectDict = CreateFrame("CHECKBUTTON", "Misspelled_cfgAutoSelectDict", cfgFrame, "OptionsCheckButtonTemplate") Misspelled_cfgAutoSelectDict:SetPoint("TOPLEFT", 20, -40) Misspelled_cfgAutoSelectDictText:SetText(L["Auto Select Dictionary to Load"]) Misspelled_cfgAutoSelectDict:SetChecked(Misspelled_DB.AutoSelectDictionary ) Misspelled_cfgAutoSelectDict:SetScript("OnClick", function(self) if self:GetChecked() then PlaySound("igMainMenuOptionCheckBoxOn") -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON else PlaySound("igMainMenuOptionCheckBoxOff") -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_OFF end Misspelled_DB.AutoSelectDictionary = not Misspelled_DB.AutoSelectDictionary --Toggle the sub options if Misspelled_DB.AutoSelectDictionary == true then Misspelled_cfgDictdeDE:Disable() Misspelled_cfgDictenGB:Disable() Misspelled_cfgDictenUS:Disable() Misspelled_cfgDictesES:Disable() Misspelled_cfgDictfrFR:Disable() Misspelled_cfgDictruRU:Disable() Misspelled_cfgDictitIT:Disable() else Misspelled_cfgDictdeDE:Enable() Misspelled_cfgDictenGB:Enable() Misspelled_cfgDictenUS:Enable() Misspelled_cfgDictesES:Enable() Misspelled_cfgDictfrFR:Enable() Misspelled_cfgDictruRU:Enable() Misspelled_cfgDictitIT:Enable() if Misspelled_DB.LoadDictionary == nil or #Misspelled_DB.LoadDictionary == 0 then Misspelled_DB.LoadDictionary = "enUS" Misspelled_cfgDictenUS:SetChecked(true) end end end) local cfgDictdeDE = CreateFrame("CHECKBUTTON", "Misspelled_cfgDictdeDE", cfgFrame, "OptionsCheckButtonTemplate") Misspelled_cfgDictdeDE:SetPoint("TOPLEFT", 40, -64) Misspelled_cfgDictdeDEText:SetText("deDE") Misspelled_cfgDictdeDE:SetChecked(Misspelled_DB.LoadDictionary == "deDE") Misspelled_cfgDictdeDE:SetScript("OnClick", function(self) if self:GetChecked() then PlaySound("igMainMenuOptionCheckBoxOn") -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON Misspelled_DB.LoadDictionary = "deDE" Misspelled_cfgDictenGB:SetChecked(false) Misspelled_cfgDictenUS:SetChecked(false) Misspelled_cfgDictesES:SetChecked(false) Misspelled_cfgDictfrFR:SetChecked(false) Misspelled_cfgDictruRU:SetChecked(false) Misspelled_cfgDictitIT:SetChecked(false) else PlaySound("igMainMenuOptionCheckBoxOff") -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_OFF end end) local cfgDictenGB = CreateFrame("CHECKBUTTON", "Misspelled_cfgDictenGB", cfgFrame, "OptionsCheckButtonTemplate") Misspelled_cfgDictenGB:SetPoint("TOPLEFT", 40, -88) Misspelled_cfgDictenGBText:SetText("enGB") Misspelled_cfgDictenGB:SetChecked(Misspelled_DB.LoadDictionary == "enGB") Misspelled_cfgDictenGB:SetScript("OnClick", function(self) if self:GetChecked() then PlaySound("igMainMenuOptionCheckBoxOn") -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON Misspelled_DB.LoadDictionary = "enGB" Misspelled_cfgDictdeDE:SetChecked(false) Misspelled_cfgDictenUS:SetChecked(false) Misspelled_cfgDictesES:SetChecked(false) Misspelled_cfgDictfrFR:SetChecked(false) Misspelled_cfgDictruRU:SetChecked(false) Misspelled_cfgDictitIT:SetChecked(false) else PlaySound("igMainMenuOptionCheckBoxOff") -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_OFF end end) local cfgDictenUS = CreateFrame("CHECKBUTTON", "Misspelled_cfgDictenUS", cfgFrame, "OptionsCheckButtonTemplate") Misspelled_cfgDictenUS:SetPoint("TOPLEFT", 40, -112) Misspelled_cfgDictenUSText:SetText("enUS") Misspelled_cfgDictenUS:SetChecked(Misspelled_DB.LoadDictionary == "enUS") Misspelled_cfgDictenUS:SetScript("OnClick", function(self) if self:GetChecked() then PlaySound("igMainMenuOptionCheckBoxOn") -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON Misspelled_DB.LoadDictionary = "enUS" Misspelled_cfgDictdeDE:SetChecked(false) Misspelled_cfgDictenGB:SetChecked(false) Misspelled_cfgDictesES:SetChecked(false) Misspelled_cfgDictfrFR:SetChecked(false) Misspelled_cfgDictruRU:SetChecked(false) Misspelled_cfgDictitIT:SetChecked(false) else PlaySound("igMainMenuOptionCheckBoxOff") -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_OFF end end) local cfgDictesES = CreateFrame("CHECKBUTTON", "Misspelled_cfgDictesES", cfgFrame, "OptionsCheckButtonTemplate") Misspelled_cfgDictesES:SetPoint("TOPLEFT", 40, -136) Misspelled_cfgDictesESText:SetText("esES") Misspelled_cfgDictesES:SetChecked(Misspelled_DB.LoadDictionary == "esES") Misspelled_cfgDictesES:SetScript("OnClick", function(self) if self:GetChecked() then PlaySound("igMainMenuOptionCheckBoxOn") -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON Misspelled_DB.LoadDictionary = "esES" Misspelled_cfgDictdeDE:SetChecked(false) Misspelled_cfgDictenGB:SetChecked(false) Misspelled_cfgDictenUS:SetChecked(false) Misspelled_cfgDictfrFR:SetChecked(false) Misspelled_cfgDictruRU:SetChecked(false) Misspelled_cfgDictitIT:SetChecked(false) else PlaySound("igMainMenuOptionCheckBoxOff") -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_OFF end end) local cfgDictfrFR = CreateFrame("CHECKBUTTON", "Misspelled_cfgDictfrFR", cfgFrame, "OptionsCheckButtonTemplate") Misspelled_cfgDictfrFR:SetPoint("TOPLEFT", 40, -160) Misspelled_cfgDictfrFRText:SetText("frFR") Misspelled_cfgDictfrFR:SetChecked(Misspelled_DB.LoadDictionary == "frFR") Misspelled_cfgDictfrFR:SetScript("OnClick", function(self) if self:GetChecked() then PlaySound("igMainMenuOptionCheckBoxOn") -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON Misspelled_DB.LoadDictionary = "frFR" Misspelled_cfgDictdeDE:SetChecked(false) Misspelled_cfgDictenGB:SetChecked(false) Misspelled_cfgDictenUS:SetChecked(false) Misspelled_cfgDictesES:SetChecked(false) Misspelled_cfgDictruRU:SetChecked(false) Misspelled_cfgDictitIT:SetChecked(false) else PlaySound("igMainMenuOptionCheckBoxOff") -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_OFF end end) local cfgDictruRU = CreateFrame("CHECKBUTTON", "Misspelled_cfgDictruRU", cfgFrame, "OptionsCheckButtonTemplate") Misspelled_cfgDictruRU:SetPoint("TOPLEFT", 40, -184) Misspelled_cfgDictruRUText:SetText("ruRU") Misspelled_cfgDictruRU:SetChecked(Misspelled_DB.LoadDictionary == "ruRU") Misspelled_cfgDictruRU:SetScript("OnClick", function(self) if self:GetChecked() then PlaySound("igMainMenuOptionCheckBoxOn") -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON Misspelled_DB.LoadDictionary = "ruRU" Misspelled_cfgDictdeDE:SetChecked(false) Misspelled_cfgDictenGB:SetChecked(false) Misspelled_cfgDictenUS:SetChecked(false) Misspelled_cfgDictfrFR:SetChecked(false) Misspelled_cfgDictesES:SetChecked(false) Misspelled_cfgDictitIT:SetChecked(false) else PlaySound("igMainMenuOptionCheckBoxOff") -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_OFF end end) local cfgDictitIT = CreateFrame("CHECKBUTTON", "Misspelled_cfgDictitIT", cfgFrame, "OptionsCheckButtonTemplate") Misspelled_cfgDictitIT:SetPoint("TOPLEFT", 40, -208) Misspelled_cfgDictitITText:SetText("itIT") Misspelled_cfgDictitIT:SetChecked(Misspelled_DB.LoadDictionary == "itIT") Misspelled_cfgDictitIT:SetScript("OnClick", function(self) if self:GetChecked() then PlaySound("igMainMenuOptionCheckBoxOn") -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON Misspelled_DB.LoadDictionary = "itIT" Misspelled_cfgDictdeDE:SetChecked(false) Misspelled_cfgDictenGB:SetChecked(false) Misspelled_cfgDictenUS:SetChecked(false) Misspelled_cfgDictfrFR:SetChecked(false) Misspelled_cfgDictesES:SetChecked(false) Misspelled_cfgDictruRU:SetChecked(false) else PlaySound("igMainMenuOptionCheckBoxOff") -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_OFF end end) --Edit User Dictionary Button local cfgEditUserDict = CreateFrame("Button", "EdutUserDictButton", cfgFrame, "UIPanelButtonTemplate") cfgEditUserDict:SetPoint("TOPLEFT", 20, -287) cfgEditUserDict:SetText(L["Edit User Dictionary..."]) cfgEditUserDict:SetWidth(200) cfgEditUserDict:SetHeight(24) cfgEditUserDict:SetScript("OnClick", function(self) --PlaySound("igMainMenuOptionCheckBoxOn") Misspelled:EditUserDict() end) --set options on startup Misspelled_cfgDictdeDE:SetChecked(false) Misspelled_cfgDictenUS:SetChecked(false) Misspelled_cfgDictenGB:SetChecked(false) Misspelled_cfgDictesES:SetChecked(false) Misspelled_cfgDictfrFR:SetChecked(false) Misspelled_cfgDictruRU:SetChecked(false) Misspelled_cfgDictitIT:SetChecked(false) if Misspelled_DB.LoadDictionary == "deDE" then Misspelled_cfgDictdeDE:SetChecked(true) elseif Misspelled_DB.LoadDictionary == "enGB" then Misspelled_cfgDictenGB:SetChecked(true) elseif Misspelled_DB.LoadDictionary == "enUS" then Misspelled_cfgDictenUS:SetChecked(true) elseif Misspelled_DB.LoadDictionary == "esES" then Misspelled_cfgDictesES:SetChecked(true) elseif Misspelled_DB.LoadDictionary == "frFR" then Misspelled_cfgDictfrFR:SetChecked(true) elseif Misspelled_DB.LoadDictionary == "ruRU" then Misspelled_cfgDictruRU:SetChecked(true) elseif Misspelled_DB.LoadDictionary == "itIT" then Misspelled_cfgDictitIT:SetChecked(true) end if Misspelled_DB.AutoSelectDictionary == true then Misspelled_cfgDictdeDE:Disable() Misspelled_cfgDictenGB:Disable() Misspelled_cfgDictenUS:Disable() Misspelled_cfgDictesES:Disable() Misspelled_cfgDictfrFR:Disable() Misspelled_cfgDictruRU:Disable() Misspelled_cfgDictitIT:Disable() else Misspelled_cfgDictdeDE:Enable() Misspelled_cfgDictenGB:Enable() Misspelled_cfgDictenUS:Enable() Misspelled_cfgDictesES:Enable() Misspelled_cfgDictfrFR:Enable() Misspelled_cfgDictruRU:Enable() Misspelled_cfgDictitIT:Enable() end --Add options frame to the list of in-game addon options --Adding addon options changed in Wow 11.0 if InterfaceOptions_AddCategory then -- Check for compatiability for older Wow clients. InterfaceOptions_AddCategory(cfgFrame) -- For Wow clients < v11 elseif Settings then -- For Wow clients > v11 local category, layout = Settings.RegisterCanvasLayoutCategory(cfgFrame, cfgFrame.name) Settings.RegisterAddOnCategory(category) end end ------------------------------------------------------------------------- -- -- Utility Routines -- ------------------------------------------------------------------------- --Split a string, at patt delimiter, into a table function Misspelled:split(str, patt) local vals = {} local valindex = 0 local word = "" -- need to add a trailing separator to catch the last value. str = str .. patt for i = 1, string_len(str) do local cha = string_sub(str, i, i) if cha ~= patt then word = word .. cha else if word ~= nil then vals[valindex] = word valindex = valindex + 1 word = "" else -- in case we get a line with no data. break end end end return vals end function Misspelled:tprint (t, indent, done) -- in case we run it standalone outside of the Wow client Lua env local Note = Note or print --local Tell = Tell or io.write -- show strings differently to distinguish them from numbers local function show (val) if type (val) == "string" then return '"' .. val .. '"' else return tostring(val) end -- if end -- show -- entry point here done = done or {} indent = indent or 0 for key, value in pairs (t) do print(string_rep(" ", indent)) -- indent it if type (value) == "table" and not done [value] then done [value] = true Note (show (key), ":"); Misspelled:tprint(value, indent + 2, done) else print(show (key), "=") print (show (value)) end end end function Misspelled:print(...) local frame = SELECTED_DOCK_FRAME or DEFAULT_CHAT_FRAME if frame then frame:AddMessage(...) end end --Check your friends list to see if the string: name is on the friends list. function Misspelled:IsFriend(name) local numFriends if GetNumFriends then numFriends = GetNumFriends() if numFriends > 0 then for i = 1, numFriends do local friendName if C_FriendList and C_FriendList.GetFriendInfoByIndex then local friend = C_FriendList.GetFriendInfoByIndex(i); friendName = friend and friend.name else friendName = GetFriendInfo(i) end if name == friendName then return true end end end end return false end --check the guild roster to see if the string: name, is a guild member. function Misspelled:IsGuildMember(name) local numGuildMembers if IsInGuild() then numGuildMembers = GetNumGuildMembers(true); -- true to include offline members if ( numGuildMembers > 0 ) then for i=1, numGuildMembers do if name == GetGuildRosterInfo(i) then return true end end end end return false end ------------------------------------------------------------------------- -- End: Utility Routines ------------------------------------------------------------------------- --[[ --testing require("bit") require("WordDict") --~ require("Dict\\Dic_en_US") --~ Misspelled_DB = {} --~ WordDict:Init() --~ ChatFrameEditBox = {} --~ function ChatFrameEditBox:GetText() --~ return "Leatherworking" --~ end s = "Test |cffff91c8A|cffxxxxxxBSecondWord|r|r" s2 = string.gsub(s, SPELLED_WRONG_HIGHLIGHT .. "(.-)|r", function(x) return x end) print(s2) ---]]