Files
ClassicAPI/AddOns/!!!ClassicAPI/APIDocumentation/APIDocumentation.lua
T
Brues 108a91a843 feat(api): describe the API in the code, browse it with /classicapi
docs/API.md is 18.7k lines and its anchors are signature slugs, so editing a
signature breaks the 281 internal and 88 README links into it. Blizzard
solved the same problem by shipping the API as data plus a browser addon,
and warcraft.wiki.gg renders those same tables. One set of descriptors can
feed all three.

Each registration now carries one next to it: typed arguments and returns,
nilability, defaults, and a sentence. Events, enumerations and returned table
shapes are described the same way. No Script_* body changed.

Game.h gains namespace Game::Doc (Field with Req/Opt/Vararg, FieldList,
Function, Method, Structure, Event) and a trailing descriptor argument on all
six Game::Lua::Register* plus Event::Custom::AutoReserve.

api/Documentation.cpp records every registration on the first pass of each
Lua state and seals it, so /reload re-registers but records nothing and the
environment comes from which pass saw the name. Export is lazy: nothing is
materialized until C_APIDocumentation.GetSystem asks for one system, because
eager export would build thousands of tables per reload for a slash command.
_classicapi_UndocumentedAPI lists what still has no descriptor.

AddOns/!!!ClassicAPI/APIDocumentation/ ports Blizzard_APIDocumentation's eight
files. Named /classicapi, not /api: the browser holds only what ClassicAPI
adds, so /api would have implied the whole 1.12 surface. Two deliberate
differences from the retail source, both noted in the file headers: a system
search also matches its namespace, since that is the name people type; and an
undocumented function says so, because an empty signature otherwise reads as
a real one.

LinkUtil gains the SetItemRef to LinkUtil.ProcessLink dispatch. That handler
registry was dead code; a new link type now registers a handler instead of
stacking another SetItemRef override. Also defines assertsafe there, which
was undefined and only mattered once the function went live.

Documented: the C_Spell and C_SpellBook surface, the spell globals, both
spellbook enumerations, the 13 UNIT_SPELLCAST_* events, GameTooltip as the
widget-method example and C_Glue as the glue and mixed-environment example.
66 of 692 function registrations. tools/New-ApiDocSkeletons.ps1 emits
paste-ready skeletons to make the rest of the sweep tractable.

Verified in game: the browser lists, searches, and prints detail; links
render and click through; right-click copies and shift-click dumps.
2026-09-12 23:20:46 -05:00

422 lines
12 KiB
Lua

-- Backport of Blizzard_APIDocumentation/Blizzard_APIDocumentation.lua to
-- vanilla 1.12 / Lua 5.0.
--
-- Implementation differences from the retail source:
-- - Lua 5.0 has no length operator (#); sequence length comes from
-- table.getn(t), matching the rest of this addon.
-- - Lua 5.0 has no `...` inside a function body; WriteLineF takes its
-- extra values from the implicit `arg` table.
-- - ChatFrameUtil does not exist. HandleOpenDump uses this client's
-- ChatFrame_OpenChat, which applies the text from ChatEdit_OnUpdate
-- rather than immediately -- so the cursor is placed one frame later,
-- after that text has landed.
local tGetN = table.getn;
APIDocumentationMixin = {};
-- "public"
function APIDocumentationMixin:OnLoad()
self.tables = {};
self.functions = {};
self.systems = {};
self.fields = {};
self.events = {};
self.callbacks = {};
self.Commands = {
Default = 1,
CopyAPI = 2,
OpenDump = 3,
};
if DEFAULT_CHAT_FRAME then
DEFAULT_CHAT_FRAME:SetMaxLines(2000);
end
end
function APIDocumentationMixin:HandleSlashCommand(command)
local commands = { (" "):split(command) };
if commands[1] == "?" or commands[1] == "help" or commands[1] == "" then
self:OutputUsage();
elseif commands[1] == "stats" then
self:OutputStats();
elseif commands[1] == "system" then
if commands[2] == "list" then
self:OutputAllSystems();
else
self:OutputUsage();
end
elseif commands[1] == "s" or commands[1] == "search" then
self:OutputAllAPIMatches(unpack(commands, 2));
elseif commands[1] then
self:TryHandlingSystemSearchCommand(unpack(commands));
else
self:OutputUsage();
end
end
function APIDocumentationMixin:HandleAPILink(link, command)
local _, type, name, parentName = (":"):split(link);
local apiInfo = self:FindAPIByName(type, name, parentName);
if apiInfo then
if command == self.Commands.CopyAPI then
self:HandleCopyAPI(apiInfo);
elseif command == self.Commands.OpenDump then
self:HandleOpenDump(apiInfo);
else
self:HandleDefaultCommand(apiInfo);
end
end
end
function APIDocumentationMixin:HandleDefaultCommand(apiInfo)
self:WriteLine(" ");
self:WriteAllLines(apiInfo:GetDetailedOutputLines());
end
function APIDocumentationMixin:HandleCopyAPI(apiInfo)
local clipboardString = apiInfo:GetClipboardString();
CopyToClipboard(clipboardString);
self:WriteLineF("Copied to clipboard: %s", clipboardString);
end
function APIDocumentationMixin:HandleOpenDump(apiInfo)
if apiInfo.Type == "Function" then
local dumpString;
local systemNamespace = apiInfo.System and apiInfo.System:GetNamespaceName() or nil;
if systemNamespace and systemNamespace ~= "" then
dumpString = ("/dump %s.%s()"):format(systemNamespace, apiInfo.Name);
else
dumpString = ("/dump %s()"):format(apiInfo.Name);
end
local desiredCursorPosition = string.len(dumpString) - 1;
ChatFrame_OpenChat(dumpString);
RunNextFrame(function()
if ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
ChatFrameEditBox:SetCursorPosition(desiredCursorPosition);
end
end);
else
self:WriteLine("Can only /dump functions");
end
end
function APIDocumentationMixin:FindAPIByName(apiType, name, parentName)
local apiTable = self:GetAPITableByTypeName(apiType);
if apiTable then
for i, apiInfo in ipairs(apiTable) do
if apiInfo:MatchesName(name, parentName) then
return apiInfo;
end
end
end
return nil;
end
function APIDocumentationMixin:GetAPITableByTypeName(apiType)
if apiType == "function" then
return self.functions;
elseif apiType == "table" then
return self.tables;
elseif apiType == "system" then
return self.systems;
elseif apiType == "field" then
return self.fields;
elseif apiType == "event" then
return self.events;
elseif apiType == "callback" then
return self.callbacks;
end
return nil;
end
function APIDocumentationMixin:OutputUsage()
self:WriteLine("Usage:");
self:WriteLine("Search for API");
self:WriteLine(self:GetIndentString() .. "/classicapi search <api name>");
self:WriteLine(self:GetIndentString() .. "or");
self:WriteLine(self:GetIndentString() .. "/classicapi s <api name>");
self:WriteLine(self:GetIndentString() .. "Example: /classicapi search spell");
self:WriteLine(" ");
self:WriteLine("List all systems");
self:WriteLine(self:GetIndentString() .. "/classicapi system list");
self:WriteLine(" ");
self:WriteLine("Search system for API");
self:WriteLine(self:GetIndentString() .. "/classicapi <system name> search <api name>");
self:WriteLine(self:GetIndentString() .. "or");
self:WriteLine(self:GetIndentString() .. "/classicapi <system name> s <api name>");
self:WriteLine(self:GetIndentString() .. "Example: /classicapi spell search cooldown");
self:WriteLine(" ");
self:WriteLine("List all API in a system");
self:WriteLine(self:GetIndentString() .. "/classicapi <system name> list");
self:WriteLine(self:GetIndentString() .. "Example: /classicapi spell list");
self:WriteLine(" ");
self:WriteLine("All searches support Lua patterns.");
end
function APIDocumentationMixin:OutputStats()
self:WriteLine("Stats:");
self:WriteLineF("Total systems: %d", tGetN(self.systems));
local totalFunctions = 0;
local totalEvents = 0;
local totalTables = 0;
for i, systemInfo in ipairs(self.systems) do
totalFunctions = totalFunctions + systemInfo:GetNumFunctions();
totalEvents = totalEvents + systemInfo:GetNumEvents();
totalTables = totalTables + systemInfo:GetNumTables();
end
self:WriteLineF("Total functions: %d", totalFunctions);
self:WriteLineF("Total events: %d", totalEvents);
self:WriteLineF("Total tables: %d", totalTables);
end
function APIDocumentationMixin:OutputAllSystems()
self:WriteLineF("All systems (%d):", tGetN(self.systems));
for i, systemInfo in ipairs(self.systems) do
self:WriteLine(systemInfo:GetSingleOutputLine());
end
end
function APIDocumentationMixin:TryHandlingSystemSearchCommand(systemName, subCommand, apiToSearchFor)
local system = self:FindSystemByName(systemName);
if system then
if subCommand == nil then
self:WriteLine(system:GetSingleOutputLine());
elseif subCommand == "s" or subCommand == "search" and apiToSearchFor then
self:OutputAllSystemAPIMatches(system, apiToSearchFor);
elseif subCommand == "list" then
self:OutputAllSystemAPI(system);
else
self:OutputUsage();
end
else
self:WriteLineF("No system found (%s)", tostring(systemName));
end
end
function APIDocumentationMixin:OutputAPIMatches(apiMatches, headerName)
if apiMatches and tGetN(apiMatches) > 0 then
self:WriteLineF("Found %d %s", tGetN(apiMatches), headerName);
for i, api in ipairs(apiMatches) do
self:WriteLine(self:GetIndentString() .. api:GetSingleOutputLine());
end
end
end
function APIDocumentationMixin:OutputAllAPIMatches(apiToSearchFor)
if not apiToSearchFor or apiToSearchFor == "" then
self:OutputUsage();
return;
end
self:WriteLine(" ");
local apiMatches = self:FindAllAPIMatches(apiToSearchFor);
if apiMatches then
local total = tGetN(apiMatches.tables) + tGetN(apiMatches.functions) + tGetN(apiMatches.events) + tGetN(apiMatches.systems) + tGetN(apiMatches.callbacks);
assert(total > 0);
self:WriteLineF("Found %d API that matches %q", total, apiToSearchFor);
self:OutputAPIMatches(apiMatches.systems, "system(s)");
self:OutputAPIMatches(apiMatches.functions, "function(s)");
self:OutputAPIMatches(apiMatches.events, "events(s)");
self:OutputAPIMatches(apiMatches.tables, "table(s)");
self:OutputAPIMatches(apiMatches.callbacks, "callback(s)");
else
self:WriteLineF("No API found that matches %q", apiToSearchFor);
end
end
function APIDocumentationMixin:OutputAllSystemAPIMatches(system, apiToSearchFor)
local apiMatches = system:FindAllAPIMatches(apiToSearchFor);
if apiMatches then
local total = tGetN(apiMatches.tables) + tGetN(apiMatches.functions) + tGetN(apiMatches.events);
assert(total > 0);
self:WriteLineF("Found %d API that matches %q", total, apiToSearchFor);
self:OutputAPIMatches(apiMatches.functions, "function(s)");
self:OutputAPIMatches(apiMatches.events, "events(s)");
self:OutputAPIMatches(apiMatches.tables, "table(s)");
else
self:WriteLineF("No API found that matches %q in %s", apiToSearchFor, system:GenerateAPILink());
end
end
function APIDocumentationMixin:OutputAllSystemAPI(system)
local apiMatches = system:ListAllAPI();
if apiMatches then
self:WriteLineF("All API in %s", system:GenerateAPILink());
self:OutputAPIMatches(apiMatches.functions, "function(s)");
self:OutputAPIMatches(apiMatches.events, "events(s)");
self:OutputAPIMatches(apiMatches.tables, "table(s)");
else
self:WriteLineF("No API found in %s", system:GenerateAPILink());
end
end
--[[static]] function APIDocumentationMixin:AddAllMatches(apiContainer, matchesContainer, apiToSearchFor)
for i, apiInfo in ipairs(apiContainer) do
if apiInfo:MatchesSearchString(apiToSearchFor) then
table.insert(matchesContainer, apiInfo);
end
end
end
function APIDocumentationMixin:FindAllAPIMatches(apiToSearchFor)
apiToSearchFor = apiToSearchFor:lower();
local matches = {
tables = {},
functions = {},
events = {},
systems = {},
callbacks = {},
};
self:AddAllMatches(self.tables, matches.tables, apiToSearchFor);
self:AddAllMatches(self.functions, matches.functions, apiToSearchFor);
self:AddAllMatches(self.systems, matches.systems, apiToSearchFor);
self:AddAllMatches(self.events, matches.events, apiToSearchFor);
self:AddAllMatches(self.callbacks, matches.callbacks, apiToSearchFor);
-- Only return something if we matched anything
for name, subTable in pairs(matches) do
if tGetN(subTable) > 0 then
return matches;
end
end
return nil;
end
function APIDocumentationMixin:FindSystemByName(systemName)
systemName = systemName:lower();
for i, systemInfo in ipairs(self.systems) do
if systemInfo:MatchesNameCaseInsenstive(systemName) then
return systemInfo;
end
end
return nil;
end
function APIDocumentationMixin:AddDocumentationTable(documentationInfo)
if documentationInfo.Name then
self:AddSystem(documentationInfo);
else
for i, tableInfo in ipairs(documentationInfo.Tables) do
self:AddTable(tableInfo);
end
end
end
function APIDocumentationMixin:WriteLine(message)
if DEFAULT_CHAT_FRAME then
local info = ChatTypeInfo["SYSTEM"];
DEFAULT_CHAT_FRAME:AddMessage(message, info.r, info.g, info.b, info.id);
else
print(message);
end
end
-- The `...` in the parameter list is what creates `arg`; a Lua 5.0 function
-- declared without it has no `arg` at all.
function APIDocumentationMixin:WriteLineF(format, ...)
self:WriteLine(string.format(format, unpack(arg)));
end
function APIDocumentationMixin:WriteAllLines(lines)
for i, line in ipairs(lines) do
self:WriteLine(line);
end
end
function APIDocumentationMixin:GetIndentString(numIndent)
return (" "):rep(numIndent or 1);
end
-- "private"
function APIDocumentationMixin:AddTable(documentationInfo)
Mixin(documentationInfo, TablesAPIMixin);
table.insert(self.tables, documentationInfo);
if documentationInfo.Fields then
for i, field in ipairs(documentationInfo.Fields) do
field.Table = documentationInfo;
self:AddField(field);
end
end
end
function APIDocumentationMixin:AddFunction(documentationInfo)
Mixin(documentationInfo, FunctionsAPIMixin);
table.insert(self.functions, documentationInfo);
if documentationInfo.Arguments then
for i, field in ipairs(documentationInfo.Arguments) do
field.Function = documentationInfo;
self:AddField(field);
end
end
if documentationInfo.Returns then
for i, field in ipairs(documentationInfo.Returns) do
field.Function = documentationInfo;
self:AddField(field);
end
end
end
function APIDocumentationMixin:AddEvent(documentationInfo)
Mixin(documentationInfo, EventsAPIMixin);
table.insert(self.events, documentationInfo);
if documentationInfo.Payload then
for i, field in ipairs(documentationInfo.Payload) do
field.Event = documentationInfo;
self:AddField(field);
end
end
end
function APIDocumentationMixin:AddField(documentationInfo)
Mixin(documentationInfo, FieldsAPIMixin);
table.insert(self.fields, documentationInfo);
end
function APIDocumentationMixin:AddSystem(documentationInfo)
Mixin(documentationInfo, SystemsAPIMixin);
table.insert(self.systems, documentationInfo);
for i, functionInfo in ipairs(documentationInfo.Functions) do
functionInfo.System = documentationInfo;
self:AddFunction(functionInfo);
end
for i, eventInfo in ipairs(documentationInfo.Events) do
eventInfo.System = documentationInfo;
self:AddEvent(eventInfo);
end
for i, tableInfo in ipairs(documentationInfo.Tables) do
tableInfo.System = documentationInfo;
self:AddTable(tableInfo);
end
end
APIDocumentation = CreateFromMixins(APIDocumentationMixin);
APIDocumentation:OnLoad();