Files
libdogtag-3-0/LibDogTag-3.0.lua
2024-06-22 14:36:17 -07:00

624 lines
19 KiB
Lua

--[[
Name: LibDogTag-3.0
Revision: @project-revision@
Website: https://www.wowace.com/projects/libdogtag-3-0
Description: A library to provide a markup syntax
]]
local MAJOR_VERSION = "LibDogTag-3.0"
local MINOR_VERSION = tonumber(("@project-date-integer@"):match("%d+")) or 33333333333333
if MINOR_VERSION > _G.DogTag_MINOR_VERSION then
_G.DogTag_MINOR_VERSION = MINOR_VERSION
end
local type, error, pairs, ipairs, next, pcall, _G = type, error, pairs, ipairs, next, pcall, _G
-- #AUTODOC_NAMESPACE DogTag
DogTag_funcs[#DogTag_funcs+1] = function(DogTag)
local L = DogTag.L
local newList, newSet, del, deepCopy = DogTag.newList, DogTag.newSet, DogTag.del, DogTag.deepCopy
local select2 = DogTag.select2
local fixNamespaceList = DogTag.fixNamespaceList
local memoizeTable = DogTag.memoizeTable
local deepCompare = DogTag.deepCompare
local kwargsToKwargTypes = DogTag.kwargsToKwargTypes
local kwargsToKwargTypesWithTableCache = DogTag.kwargsToKwargTypesWithTableCache
local fsNeedUpdate, fsNeedQuickUpdate, codeToFunction, codeToEventList, eventData, clearCodes
local clearCodes = DogTag.clearCodes
DogTag_funcs[#DogTag_funcs+1] = function()
fsNeedUpdate = DogTag.fsNeedUpdate
fsNeedQuickUpdate = DogTag.fsNeedQuickUpdate
codeToFunction = DogTag.codeToFunction
codeToEventList = DogTag.codeToEventList
eventData = DogTag.eventData
end
local fsToFrame, fsToCode, fsToNSList, fsToKwargs, Tags, AddonFinders
if DogTag.oldLib then
local oldLib = DogTag.oldLib
fsToFrame = oldLib.fsToFrame
fsToCode = oldLib.fsToCode
fsToNSList = oldLib.fsToNSList
fsToKwargs = oldLib.fsToKwargs
local tmp = {}
for fs, kwargs in pairs(fsToKwargs) do
tmp[fs] = memoizeTable(deepCopy(kwargs))
fsToKwargs[fs] = nil
end
for fs, kwargs in pairs(tmp) do
fsToKwargs[fs] = kwargs
end
tmp = nil
Tags = oldLib.Tags
Tags["Base"] = {}
AddonFinders = oldLib.AddonFinders
AddonFinders["Base"] = {}
else
fsToFrame = {}
fsToCode = {}
fsToNSList = {}
fsToKwargs = {}
Tags = { ["Base"] = {} }
AddonFinders = { ["Base"] = {} }
end
DogTag.fsToFrame = fsToFrame
DogTag.fsToCode = fsToCode
DogTag.fsToNSList = fsToNSList
DogTag.fsToKwargs = fsToKwargs
DogTag.Tags = Tags
DogTag.AddonFinders = AddonFinders
local sortStringList = DogTag.sortStringList
DogTag.__colors = {
minHP = { 1, 0, 0 },
midHP = { 1, 1, 0 },
maxHP = { 0, 1, 0 },
unknown = { 0.8, 0.8, 0.8 },
hostile = { 226/255, 45/255, 75/255 },
neutral = { 1, 1, 34/255 },
friendly = { 0.2, 0.8, 0.15 },
civilian = { 48/255, 113/255, 191/255 },
dead = { 0.6, 0.6, 0.6 },
tapped = { 0.5, 0.5, 0.5 },
disconnected = { 0.7, 0.7, 0.7 },
petHappy = { 0, 1, 0 },
petNeutral = { 1, 1, 0 },
petAngry = { 1, 0, 0 },
rage = { 226/255, 45/255, 75/255 },
energy = { 1, 220/255, 25/255 },
focus = { 1, 210/255, 0 },
mana = { 48/255, 113/255, 191/255 },
runicPower = { 0, 209/255, 1 },
}
local function updateClassColors()
local classColors = _G.CUSTOM_CLASS_COLORS or _G.RAID_CLASS_COLORS
for class, data in pairs(classColors) do
DogTag.__colors[class] = { data.r, data.g, data.b, }
end
end
updateClassColors()
--[[
Notes:
Adds a tag to the specified namespace
Arguments:
string - namespace to add to
string - name of the tag
table - data of the tag
Example:
LibStub("LibDogTag-3.0"):AddTag("MyNamespace", "Square", {
code = function(number) -- actual function that will be called
return number * number
end,
arg = {
'number', 'number', '@req', -- name, types, default
},
ret = 'number', -- return value
events = "SOME_EVENT#$number", -- will update when SOME_EVENT with the argument `number` is dispatched
doc = "Return the square of number", -- the description
example = '[4:Square] => "16"; [5:Square] => "25"', -- show one or more examples in this format
category = "Category name",
})
]]
function DogTag:AddTag(namespace, tag, data)
if type(namespace) ~= "string" then
error(("Bad argument #2 to `AddTag'. Expected %q, got %q"):format("string", type(namespace)), 2)
end
if type(tag) ~= "string" then
error(("Bad argument #3 to `AddTag'. Expected %q, got %q"):format("string", type(tag)), 2)
end
if type(data) ~= "table" then
error(("Bad argument #4 to `AddTag'. Expected %q, got %q"):format("table", type(data)), 2)
end
if not Tags[namespace] then
Tags[namespace] = {}
end
if Tags["Base"][tag] or Tags[namespace][tag] then
error(("Bad argument #3 to `AddTag'. %q already registered"):format(tag), 2)
end
local tagData = newList()
Tags[namespace][tag] = tagData
local arg = data.arg
if arg then
if type(arg) ~= "table" then
error("arg must be a table", 2)
end
if #arg % 3 ~= 0 then
error("arg must be a table with a length a multiple of 3", 2)
end
for i = 1, #arg, 3 do
local key, types, default = arg[i], arg[i+1], arg[i+2]
if type(key) ~= "string" then
error("arg must have its keys as strings", 2)
end
if type(types) ~= "string" then
error("arg must have its types as strings", 2)
end
if types:match("^tuple%-") then
if key ~= "..." then
error("arg must have its key be ... if it is a tuple.", 2)
end
local tupleTypes = types:sub(7)
local t = newSet((';'):split(tupleTypes))
for k in pairs(t) do
if k ~= "nil" and k ~= "number" and k ~= "string" and k ~= "boolean" then
error("arg can only have tuples of nil, number, string, or boolean", 2)
end
end
if t["boolean"] and (next(t, "boolean") or next(t) ~= "boolean") then
error("arg cannot specify both boolean and something else", 2)
end
t = del(t)
arg[i+1] = "tuple-" .. sortStringList(tupleTypes)
else
local t = newSet((';'):split(types))
for k in pairs(t) do
if k ~= "nil" and k ~= "number" and k ~= "string" and k ~= "undef" and k ~= "boolean" then
error("arg must have nil, number, string, undef, boolean, or tuple", 2)
end
end
if not key:match("^[a-z]+$") then
error("arg must have its key be a string of lowercase letters.", 2)
end
if t["nil"] and t["undef"] then
error("arg cannot specify both nil and undef", 2)
end
if t["boolean"] and (next(t, "boolean") or next(t) ~= "boolean") then
error("arg cannot specify both boolean and something else", 2)
end
t = del(t)
arg[i+1] = sortStringList(types)
end
end
tagData.arg = arg
end
if data.alias then
if type(data.alias) == "string" then
tagData.alias = data.alias
else -- function
tagData.alias = data.alias()
tagData.aliasFunc = data.alias
end
else
local ret = data.ret
if type(ret) == "string" then
tagData.ret = sortStringList(ret)
if ret then
local rets = newSet((";"):split(ret))
for k in pairs(rets) do
if k ~= "nil" and k ~= "number" and k ~= "string" and k ~= "boolean" then
error("ret must have nil, number, string, or boolean", 2)
end
end
rets = del(rets)
end
elseif type(ret) == "function" then
tagData.ret = ret
else
error(("ret must be a string or a function which returns a string, got %s"):format(type(ret)), 2)
end
if data.events then
if type(data.events) == "string" then
tagData.events = sortStringList(data.events)
elseif type(data.events) == "function" then
tagData.events = data.events
else
error(("events must be a string, function, or nil, got %s"):format(type(data.events)), 2)
end
end
tagData.alias = data.fakeAlias
if type(data) == "function" then
tagData.static = data.static
else
tagData.static = data.static and true or nil
end
if tagData.static and tagData.events then
error("Cannot specify both static and events", 2)
end
if type(data.code) ~= "function" then
error(("code must be a function, got %s"):format(type(data.code)), 2)
end
tagData.code = data.code
tagData.dynamicCode = data.dynamicCode and true or nil
end
tagData.doc = data.doc
if data.doc and type(data.doc) ~= "string" then
error(("doc must be nil or a string, got %s"):format(type(data.doc)), 2)
end
tagData.example = data.example
if data.doc then
if type(data.example) ~= "string" then
error(("if doc is supplied, example must be a string, got %s"):format(type(data.example)), 2)
end
local examples = newList((";"):split(data.example))
for i, v in ipairs(examples) do
if not v:trim():match("^%[.*%] => \".*\"$") then
error(("example must be in the form of [Tag sequence] => \"Result\", %s is not in said form."):format(v:trim()))
end
end
else
if data.example then
error("if doc is not supplied, example must be nil", 2)
end
end
tagData.category = data.category
if data.doc then
if type(data.category) ~= "string" then
error(("if doc is supplied, category must be a string, got %s"):format(type(data.category)), 2)
end
else
if data.category then
error("if doc is not supplied, category must be nil", 2)
end
end
if data.noDoc and type(data.doc) ~= "nil" then
error(("doc must be nil if noDoc is true, got %s"):format(type(data.doc)), 2)
end
tagData.noDoc = data.noDoc
del(data)
clearCodes(namespace)
end
local function updateFontString(fs)
fsNeedUpdate[fs] = nil
fsNeedQuickUpdate[fs] = nil
local code = fsToCode[fs]
if code then
local nsList = fsToNSList[fs]
local kwargs = fsToKwargs[fs]
local kwargTypes = kwargsToKwargTypesWithTableCache[kwargs]
local func = codeToFunction[nsList][kwargTypes][code]
DogTag.__isMouseOver = DogTag.__lastMouseover == fsToFrame[fs]
local success, text, opacity, outline = pcall(func, kwargs)
if not success then
DogTag.tagError(code, nsList, text)
return
end
if success then
fs:SetText(text)
if opacity then
fs:SetAlpha(opacity)
end
local a, b, c = fs:GetFont()
if c ~= (outline or '') then
fs:SetFont(a, b, outline or '')
end
end
end
end
DogTag.updateFontString = updateFontString
--[[
Notes:
Manually updates a FontString previously registered.
Arguments:
frame - the FontString previously registered
Example:
LibStub("LibDogTag-3.0"):UpdateFontString(fs)
]]
function DogTag:UpdateFontString(fs)
local code = fsToCode[fs]
if code then
updateFontString(fs)
end
end
--[[
Notes:
Manually updates all FontStrings on a specified frame.
Arguments:
frame - the frame which to update all FontStrings on
Example:
LibStub("LibDogTag-3.0"):UpdateAllForFrame(frame)
]]
function DogTag:UpdateAllForFrame(frame)
for fs, f in pairs(fsToFrame) do
if frame == f then
updateFontString(fs)
end
end
end
--[[
Notes:
Adds a FontString to LibDogTag-3.0's registry, which will be updated automatically.
You can add twice without removing. It will just overwrite the previous registration.
You can specify any number of namespaces. "Base" is always included as a namespace.
The kwargs table is optional and always goes on the end after the namespaces. You can recycle the table after registering.
Arguments:
frame - the FontString to register
frame - the Frame which holds the FontString
string - the tag sequence
[optional] string - a semicolon-separated list of namespaces. Base is implied
[optional] table - a dictionary of default kwargs for all tags in the code to receive
Example:
LibStub("LibDogTag-3.0"):AddFontString(fs, fs:GetParent(), "[Name]", "Unit", { unit = 'mouseover' })
LibStub("LibDogTag-3.0"):AddFontString(fs, fs:GetParent(), "[Tag]", "MyNamespace")
LibStub("LibDogTag-3.0"):AddFontString(fs, fs:GetParent(), "[Tag] [Name]", "MyNamespace;Unit", { value = 5, unit = 'player', }) -- two namespaces at once
]]
function DogTag:AddFontString(fs, frame, code, nsList, kwargs)
if type(fs) ~= "table" then
error(("Bad argument #2 to `AddFontString'. Expected %q, got %q."):format("table", type(fs)), 2)
elseif type(frame) ~= "table" then
error(("Bad argument #3 to `AddFontString'. Expected %q, got %q."):format("table", type(frame)), 2)
elseif type(code) ~= "string" then
error(("Bad argument #4 to `AddFontString'. Expected %q, got %q."):format("string", type(code)), 2)
elseif nsList and type(nsList) ~= "string" then
error(("Bad argument #5 to `AddFontString'. Expected %q, got %q."):format("string", type(nsList)), 2)
elseif kwargs and type(kwargs) ~= "table" then
error(("Bad argument #6 to `AddFontString'. Expected %q, got %q."):format("table", type(kwargs)), 2)
end
nsList = fixNamespaceList[nsList]
--[[ Cybeloras 7-4-2012:
Noticed a massive performance bottleneck in this function, and this is what I discovered:
Using a kwargs table of {color=true,group=3,icons=108} (a pretty normal table, just 3 values),
the following were the results of two different methods to test if the tables are the same:
Over 500,000 iterations:
memoizeTable(deepCopy(kwargs)) averaged an execution time of 0.0270833 seconds
deepCompare(fsToKwargs[fs], kwargs) averaged an execution time of 0.0038213 seconds
Giving a 608.74% speed advantage to deepCompare
If we deepCompare first to see if we can return early (which should happen almost all the time,
because kwargs change very infrequently), the performance boost is massive. If deepCompare reveals that the kwargs have changed,
then we can go on to memoizeTable for the rest of the function.
Code changes:
Moved ( kwargs = memoizeTable(deepCopy(kwargs)) ) down below the if block
Changed ( fsToKwargs[fs] == kwargs ) to ( deepCompare(fsToKwargs[fs], kwargs) )
Added function DogTag.deepCompare to Helpers.lua
Added deepCompare = DogTag.deepCompare upvalue to this file's header
]]
if fsToCode[fs] then
if fsToFrame[fs] == frame and fsToCode[fs] == code and fsToNSList[fs] == nsList and deepCompare(fsToKwargs[fs], kwargs) then
fsNeedUpdate[fs] = true
return
end
self:RemoveFontString(fs)
end
kwargs = memoizeTable(deepCopy(kwargs))
fsToFrame[fs] = frame
fsToCode[fs] = code
fsToNSList[fs] = nsList
fsToKwargs[fs] = kwargs
local kwargTypes = kwargsToKwargTypesWithTableCache[kwargs]
local codeToEventList_nsList_kwargTypes_code = codeToEventList[nsList][kwargTypes][code]
if codeToEventList_nsList_kwargTypes_code == nil then
local _ = codeToFunction[nsList][kwargTypes][code] -- i guess this is just to invoke a metamethod. everybody loves commented code!
codeToEventList_nsList_kwargTypes_code = codeToEventList[nsList][kwargTypes][code]
if codeToEventList_nsList_kwargTypes_code == nil then
local _, minor = LibStub(MAJOR_VERSION)
error(("%s.%d: Error with code %q (%s). Event list not created. Please inform ckknight."):format(MAJOR_VERSION, minor, code, nsList))
end
end
if codeToEventList_nsList_kwargTypes_code then
for event, arg in pairs(codeToEventList_nsList_kwargTypes_code) do
eventData[event][fs] = arg
DogTag.eventUsed(event)
end
end
updateFontString(fs)
end
--[[
Notes:
Removes a registered FontString from LibDogTag-3.0's registry.
You can remove twice without issues.
Arguments:
frame - the FontString previously registered
Example:
LibStub("LibDogTag-3.0"):RemoveFontString(fs)
]]
function DogTag:RemoveFontString(fs)
if type(fs) ~= "table" then
error(("Bad argument #2 to `RemoveFontString'. Expected %q, got %q"):format("table", type(fs)), 2)
end
local code = fsToCode[fs]
if not code then
return
end
local frame = fsToFrame[fs]
local nsList = fsToNSList[fs]
local kwargs = fsToKwargs[fs]
fsToCode[fs], fsToFrame[fs], fsToNSList[fs], fsToKwargs[fs] = nil, nil, nil, nil
fsNeedUpdate[fs], fsNeedQuickUpdate[fs] = nil, nil
local kwargTypes = kwargsToKwargTypesWithTableCache[kwargs]
local codeToEventList_nsList_kwargTypes_code = codeToEventList[nsList][kwargTypes][code]
if codeToEventList_nsList_kwargTypes_code then
for event in pairs(codeToEventList_nsList_kwargTypes_code) do
eventData[event][fs] = nil
end
end
fs:SetText(nil)
local a, b = fs:GetFont()
fs:SetFont(a, b, "")
end
--[[
Notes:
Adds a handler to be called when an addon or library comes into being
This should only really be called by sublibraries or addons that register tags.
Arguments:
string - namespace the addon finder is associated with
string - "_G" for a value on the global table or "LibStub", "Rock", "AceLibrary" for a library of the specified type
string - name of the addon or library
function - function to be called when addon or library is found
Example:
LibStub("DogTag-3.0"):AddAddonFinder("MyNamespace", "LibStub", "LibMonkey-1.0", function(LibMonkey)
-- do something with LibMonkey now
end)
]]
function DogTag:AddAddonFinder(namespace, kind, name, func)
if type(namespace) ~= "string" then
error(("Bad argument #2 to `AddAddonFinder'. Expected %q, got %q"):format("string", type(namespace)), 2)
end
if type(kind) ~= "string" then
error(("Bad argument #3 to `AddAddonFinder'. Expected %q, got %q"):format("string", type(kind)), 2)
end
if kind ~= "_G" and kind ~= "LibStub" and kind ~= "Rock" and kind ~= "AceLibrary" then
error(("Bad argument #3 to `AddAddonFinder'. Expected %q, %q, %q or %q, got %q"):format("_G", "LibStub", "Rock", "AceLibrary", kind), 2)
end
if type(name) ~= "string" then
error(("Bad argument #4 to `AddAddonFinder'. Expected %q, got %q"):format("string", type(name)), 2)
end
if type(func) ~= "function" then
error(("Bad argument #5 to `AddAddonFinder'. Expected %q, got %q"):format("function", type(func)), 2)
end
if not AddonFinders[namespace] then
AddonFinders[namespace] = {}
end
AddonFinders[namespace][newList(kind, name, func)] = true
end
local inADDON_LOADED = false
local accessed_ADDON_LOADED = false
function DogTag:ADDON_LOADED()
if inADDON_LOADED then
accessed_ADDON_LOADED = true
return
end
inADDON_LOADED = true
accessed_ADDON_LOADED = false
for namespace, data in pairs(AddonFinders) do
local refresh = false
for k in pairs(data) do
local kind, name, func = k[1], k[2], k[3]
if kind == "_G" then
if _G[name] then
data[k] = nil
del(k)
func(_G[name])
refresh = true
end
elseif kind == "AceLibrary" then
if AceLibrary and AceLibrary:HasInstance(name) then
data[k] = nil
del(k)
func(AceLibrary(name))
refresh = true
end
elseif kind == "Rock" then
if Rock and Rock:HasLibrary(name) then
data[k] = nil
del(k)
func(Rock:GetLibrary(name))
refresh = true
end
elseif kind == "LibStub" then
if Rock then
Rock:HasLibrary(name) -- try to load
end
if AceLibrary then
AceLibrary:HasInstance(name) -- try to load
end
(C_AddOns and C_AddOns.LoadAddOn or _G.LoadAddOn)(name)
if LibStub:GetLibrary(name, true) then
data[k] = nil
del(k)
func(LibStub:GetLibrary(name))
refresh = true
end
end
end
if refresh then
clearCodes(namespace)
end
end
inADDON_LOADED = false
if accessed_ADDON_LOADED then
self:ADDON_LOADED()
end
end
--[[
Notes:
Clears a namespace's tags and any other relevant data.
This should be called when a sublibrary is upgrading.
Arguments:
string - namespace that is to be cleared
Example:
LibStub("LibDogTag-3.0"):ClearNamespace("MyNamespace")
]]
function DogTag:ClearNamespace(namespace)
if type(namespace) ~= "string" then
error(("Bad argument #2 to `ClearNamespace'. Expected %q, got %q"):format("string", type(namespace)), 2)
end
Tags[namespace] = nil
AddonFinders[namespace] = nil
self.EventHandlers[namespace] = nil
self.TimerHandlers[namespace] = nil
clearCodes(namespace)
collectgarbage('collect')
end
local function updateAllFontStrings()
for fs in pairs(fsToFrame) do
updateFontString(fs)
end
end
local function updateAllClassColors()
updateClassColors()
updateAllFontStrings()
end
function DogTag:PLAYER_LOGIN()
updateClassColors()
if _G.CUSTOM_CLASS_COLORS then
_G.CUSTOM_CLASS_COLORS:RegisterCallback(updateAllClassColors)
end
end
function DogTag:UnregisterCustomClassColors()
if _G.CUSTOM_CLASS_COLORS then
_G.CUSTOM_CLASS_COLORS:UnregisterCallback(updateAllClassColors)
end
end
end