-- main.lua
-- Implements the plugin entrypoint (in this case the entire plugin)
-- Global variables:
local g_Plugin = nil
local g_PluginFolder = ""
local g_Stats = {}
local g_TrackedPages = {}
local function LoadAPIFiles(a_Folder, a_DstTable)
assert(type(a_Folder) == "string")
assert(type(a_DstTable) == "table")
local Folder = g_PluginFolder .. a_Folder;
for _, fnam in ipairs(cFile:GetFolderContents(Folder)) do
local FileName = Folder .. fnam;
-- We only want .lua files from the folder:
if (cFile:IsFile(FileName) and fnam:match(".*%.lua$")) then
local TablesFn, Err = loadfile(FileName);
if (type(TablesFn) ~= "function") then
LOGWARNING("Cannot load API descriptions from " .. FileName .. ", Lua error '" .. Err .. "'.");
else
local Tables = TablesFn();
if (type(Tables) ~= "table") then
LOGWARNING("Cannot load API descriptions from " .. FileName .. ", returned object is not a table (" .. type(Tables) .. ").");
break
end
for k, cls in pairs(Tables) do
a_DstTable[k] = cls;
end
end -- if (TablesFn)
end -- if (is lua file)
end -- for fnam - Folder[]
end
local function CreateAPITables()
--[[
We want an API table of the following shape:
local API = {
{
Name = "cCuboid",
Functions = {
{Name = "Sort"},
{Name = "IsInside"}
},
Constants = {
},
Variables = {
},
Descendants = {}, -- Will be filled by ReadDescriptions(), array of class APIs (references to other member in the tree)
},
{
Name = "cBlockArea",
Functions = {
{Name = "Clear"},
{Name = "CopyFrom"},
...
},
Constants = {
{Name = "baTypes", Value = 0},
{Name = "baMetas", Value = 1},
...
},
Variables = {
},
...
},
cCuboid = {} -- Each array item also has the map item by its name
};
local Globals = {
Functions = {
...
},
Constants = {
...
}
};
--]]
local Globals = {Functions = {}, Constants = {}, Variables = {}, Descendants = {}};
local API = {};
local function Add(a_APIContainer, a_ObjName, a_ObjValue)
if (type(a_ObjValue) == "function") then
table.insert(a_APIContainer.Functions, {Name = a_ObjName});
elseif (
(type(a_ObjValue) == "number") or
(type(a_ObjValue) == "string")
) then
table.insert(a_APIContainer.Constants, {Name = a_ObjName, Value = a_ObjValue});
end
end
local function ParseClass(a_ClassName, a_ClassObj)
local res = {Name = a_ClassName, Functions = {}, Constants = {}, Variables = {}, Descendants = {}};
-- Add functions and constants:
for i, v in pairs(a_ClassObj) do
Add(res, i, v);
end
-- Member variables:
local SetField = a_ClassObj[".set"] or {};
if ((a_ClassObj[".get"] ~= nil) and (type(a_ClassObj[".get"]) == "table")) then
for k in pairs(a_ClassObj[".get"]) do
if (SetField[k] == nil) then
-- It is a read-only variable, add it as a constant:
table.insert(res.Constants, {Name = k, Value = ""});
else
-- It is a read-write variable, add it as a variable:
table.insert(res.Variables, { Name = k });
end
end
end
return res;
end
for i, v in pairs(_G) do
if (
(v ~= _G) and -- don't want the global namespace
(v ~= _G.packages) and -- don't want any packages
(v ~= _G[".get"]) and
(v ~= g_APIDesc)
) then
if (type(v) == "table") then
local cls = ParseClass(i, v)
table.insert(API, cls);
API[cls.Name] = cls
else
Add(Globals, i, v);
end
end
end
return API, Globals;
end
local function WriteArticles(f)
f:write([[
The following articles provide various extra information on plugin development
]]);
for _, extra in ipairs(g_APIDesc.ExtraPages) do
local SrcFileName = g_PluginFolder .. "/" .. extra.FileName;
if (cFile:Exists(SrcFileName)) then
local DstFileName = "API/" .. extra.FileName;
if (cFile:Exists(DstFileName)) then
cFile:Delete(DstFileName);
end
cFile:Copy(SrcFileName, DstFileName);
f:write("
");
end
-- Make a link out of anything with the special linkifying syntax {{link|title}}
local function LinkifyString(a_String, a_Referrer)
assert(a_Referrer ~= nil);
assert(a_Referrer ~= "");
--- Adds a page to the list of tracked pages (to be checked for existence at the end)
local function AddTrackedPage(a_PageName)
local Pg = (g_TrackedPages[a_PageName] or {});
table.insert(Pg, a_Referrer);
g_TrackedPages[a_PageName] = Pg;
end
--- Creates the HTML for the specified link and title
local function CreateLink(Link, Title)
if (Link:sub(1, 7) == "http://") then
-- The link is a full absolute URL, do not modify, do not track:
return "" .. Title .. "";
end
local idxHash = Link:find("#");
if (idxHash ~= nil) then
-- The link contains an anchor:
if (idxHash == 1) then
-- Anchor in the current page, no need to track:
return "" .. Title .. "";
end
-- Anchor in another page:
local PageName = Link:sub(1, idxHash - 1);
AddTrackedPage(PageName);
return "" .. Title .. "";
end
-- Link without anchor:
AddTrackedPage(Link);
return "" .. Title .. "";
end
-- Linkify the strings using the CreateLink() function:
local txt = a_String:gsub("{{([^|}]*)|([^}]*)}}", CreateLink) -- {{link|title}}
txt = txt:gsub("{{([^|}]*)}}", -- {{LinkAndTitle}}
function(LinkAndTitle)
local idxHash = LinkAndTitle:find("#");
if (idxHash ~= nil) then
-- The LinkAndTitle contains a hash, remove the hashed part from the title:
return CreateLink(LinkAndTitle, LinkAndTitle:sub(1, idxHash - 1));
end
return CreateLink(LinkAndTitle, LinkAndTitle);
end
);
return txt;
end
local function WriteHtmlHook(a_Hook, a_HookNav)
local fnam = "API/" .. a_Hook.DefaultFnName .. ".html";
local f, error = io.open(fnam, "w");
if (f == nil) then
LOG("Cannot write \"" .. fnam .. "\": \"" .. error .. "\".");
return;
end
local HookName = a_Hook.DefaultFnName;
f:write([[
MCServer API - ]], HookName, [[ Hook
The default name for the callback function is ");
f:write(a_Hook.DefaultFnName, ". It has the following signature:\n");
f:write("
function ", HookName, "(");
if (a_Hook.Params == nil) then
a_Hook.Params = {};
end
for i, param in ipairs(a_Hook.Params) do
if (i > 1) then
f:write(", ");
end
f:write(param.Name);
end
f:write(")
\n
Parameters:
\n
Name
Type
Notes
\n");
for _, param in ipairs(a_Hook.Params) do
f:write("
", param.Name, "
", LinkifyString(param.Type, HookName), "
", LinkifyString(param.Notes, HookName), "
\n");
end
f:write("
\n
" .. LinkifyString(a_Hook.Returns or "", HookName) .. "
A plugin can register to be called whenever an "interesting event" occurs. It does so by calling
cPluginManager's AddHook() function and implementing a callback
function to handle the event.
A plugin can decide whether it will let the event pass through to the rest of the plugins, or hide it
from them. This is determined by the return value from the hook callback function. If the function
returns false or no value, the event is propagated further. If the function returns true, the processing
is stopped, no other plugin receives the notification (and possibly MCServer disables the default
behavior for the event). See each hook's details to see the exact behavior.
Hook name
Called when
]]);
for _, hook in ipairs(a_Hooks) do
if (hook.DefaultFnName == nil) then
-- The hook is not documented yet
f:write("
\n");
WriteHtmlHook(hook, a_HookNav);
end
end
f:write([[
]]);
end
local function ReadDescriptions(a_API)
-- Returns true if the class of the specified name is to be ignored
local function IsClassIgnored(a_ClsName)
if (g_APIDesc.IgnoreClasses == nil) then
return false;
end
for _, name in ipairs(g_APIDesc.IgnoreClasses) do
if (a_ClsName:match(name)) then
return true;
end
end
return false;
end
-- Returns true if the function is to be ignored
local function IsFunctionIgnored(a_ClassName, a_FnName)
if (g_APIDesc.IgnoreFunctions == nil) then
return false;
end
if (((g_APIDesc.Classes[a_ClassName] or {}).Functions or {})[a_FnName] ~= nil) then
-- The function is documented, don't ignore
return false;
end
local FnName = a_ClassName .. "." .. a_FnName;
for _, name in ipairs(g_APIDesc.IgnoreFunctions) do
if (FnName:match(name)) then
return true;
end
end
return false;
end
-- Returns true if the constant (specified by its fully qualified name) is to be ignored
local function IsConstantIgnored(a_CnName)
if (g_APIDesc.IgnoreConstants == nil) then
return false;
end;
for _, name in ipairs(g_APIDesc.IgnoreConstants) do
if (a_CnName:match(name)) then
return true;
end
end
return false;
end
-- Returns true if the member variable (specified by its fully qualified name) is to be ignored
local function IsVariableIgnored(a_VarName)
if (g_APIDesc.IgnoreVariables == nil) then
return false;
end;
for _, name in ipairs(g_APIDesc.IgnoreVariables) do
if (a_VarName:match(name)) then
return true;
end
end
return false;
end
-- Remove ignored classes from a_API:
local APICopy = {};
for _, cls in ipairs(a_API) do
if not(IsClassIgnored(cls.Name)) then
table.insert(APICopy, cls);
end
end
for i = 1, #a_API do
a_API[i] = APICopy[i];
end;
-- Process the documentation for each class:
for _, cls in ipairs(a_API) do
-- Initialize default values for each class:
cls.ConstantGroups = {};
cls.NumConstantsInGroups = 0;
cls.NumConstantsInGroupsForDescendants = 0;
-- Rename special functions:
for _, fn in ipairs(cls.Functions) do
if (fn.Name == ".call") then
fn.DocID = "constructor";
fn.Name = "() (constructor)";
elseif (fn.Name == ".add") then
fn.DocID = "operator_plus";
fn.Name = "operator +";
elseif (fn.Name == ".div") then
fn.DocID = "operator_div";
fn.Name = "operator /";
elseif (fn.Name == ".mul") then
fn.DocID = "operator_mul";
fn.Name = "operator *";
elseif (fn.Name == ".sub") then
fn.DocID = "operator_sub";
fn.Name = "operator -";
elseif (fn.Name == ".eq") then
fn.DocID = "operator_eq";
fn.Name = "operator ==";
end
end
local APIDesc = g_APIDesc.Classes[cls.Name];
if (APIDesc ~= nil) then
APIDesc.IsExported = true;
cls.Desc = APIDesc.Desc;
cls.AdditionalInfo = APIDesc.AdditionalInfo;
-- Process inheritance:
if (APIDesc.Inherits ~= nil) then
for _, icls in ipairs(a_API) do
if (icls.Name == APIDesc.Inherits) then
table.insert(icls.Descendants, cls);
cls.Inherits = icls;
end
end
end
cls.UndocumentedFunctions = {}; -- This will contain names of all the functions that are not documented
cls.UndocumentedConstants = {}; -- This will contain names of all the constants that are not documented
cls.UndocumentedVariables = {}; -- This will contain names of all the variables that are not documented
local DoxyFunctions = {}; -- This will contain all the API functions together with their documentation
local function AddFunction(a_Name, a_Params, a_Return, a_Notes)
table.insert(DoxyFunctions, {Name = a_Name, Params = a_Params, Return = a_Return, Notes = a_Notes});
end
if (APIDesc.Functions ~= nil) then
-- Assign function descriptions:
for _, func in ipairs(cls.Functions) do
local FnName = func.DocID or func.Name;
local FnDesc = APIDesc.Functions[FnName];
if (FnDesc == nil) then
-- No description for this API function
AddFunction(func.Name);
if not(IsFunctionIgnored(cls.Name, FnName)) then
table.insert(cls.UndocumentedFunctions, FnName);
end
else
-- Description is available
if (FnDesc[1] == nil) then
-- Single function definition
AddFunction(func.Name, FnDesc.Params, FnDesc.Return, FnDesc.Notes);
else
-- Multiple function overloads
for _, desc in ipairs(FnDesc) do
AddFunction(func.Name, desc.Params, desc.Return, desc.Notes);
end -- for k, desc - FnDesc[]
end
FnDesc.IsExported = true;
end
end -- for j, func
-- Replace functions with their described and overload-expanded versions:
cls.Functions = DoxyFunctions;
else -- if (APIDesc.Functions ~= nil)
for _, func in ipairs(cls.Functions) do
local FnName = func.DocID or func.Name;
if not(IsFunctionIgnored(cls.Name, FnName)) then
table.insert(cls.UndocumentedFunctions, FnName);
end
end
end -- if (APIDesc.Functions ~= nil)
if (APIDesc.Constants ~= nil) then
-- Assign constant descriptions:
for _, cons in ipairs(cls.Constants) do
local CnDesc = APIDesc.Constants[cons.Name];
if (CnDesc == nil) then
-- Not documented
if not(IsConstantIgnored(cls.Name .. "." .. cons.Name)) then
table.insert(cls.UndocumentedConstants, cons.Name);
end
else
cons.Notes = CnDesc.Notes;
CnDesc.IsExported = true;
end
end -- for j, cons
else -- if (APIDesc.Constants ~= nil)
for _, cons in ipairs(cls.Constants) do
if not(IsConstantIgnored(cls.Name .. "." .. cons.Name)) then
table.insert(cls.UndocumentedConstants, cons.Name);
end
end
end -- else if (APIDesc.Constants ~= nil)
-- Assign member variables' descriptions:
if (APIDesc.Variables ~= nil) then
for _, var in ipairs(cls.Variables) do
local VarDesc = APIDesc.Variables[var.Name];
if (VarDesc == nil) then
-- Not documented
if not(IsVariableIgnored(cls.Name .. "." .. var.Name)) then
table.insert(cls.UndocumentedVariables, var.Name);
end
else
-- Copy all documentation:
for k, v in pairs(VarDesc) do
var[k] = v
end
end
end -- for j, var
else -- if (APIDesc.Variables ~= nil)
for _, var in ipairs(cls.Variables) do
if not(IsVariableIgnored(cls.Name .. "." .. var.Name)) then
table.insert(cls.UndocumentedVariables, var.Name);
end
end
end -- else if (APIDesc.Variables ~= nil)
if (APIDesc.ConstantGroups ~= nil) then
-- Create links between the constants and the groups:
local NumInGroups = 0;
local NumInDescendantGroups = 0;
for j, group in pairs(APIDesc.ConstantGroups) do
group.Name = j;
group.Constants = {};
if (type(group.Include) == "string") then
group.Include = { group.Include };
end
local NumInGroup = 0;
for _, incl in ipairs(group.Include or {}) do
for _, cons in ipairs(cls.Constants) do
if ((cons.Group == nil) and cons.Name:match(incl)) then
cons.Group = group;
table.insert(group.Constants, cons);
NumInGroup = NumInGroup + 1;
end
end -- for cidx - cls.Constants[]
end -- for idx - group.Include[]
NumInGroups = NumInGroups + NumInGroup;
if (group.ShowInDescendants) then
NumInDescendantGroups = NumInDescendantGroups + NumInGroup;
end
-- Sort the constants:
table.sort(group.Constants,
function(c1, c2)
return (c1.Name < c2.Name);
end
);
end -- for j - APIDesc.ConstantGroups[]
cls.ConstantGroups = APIDesc.ConstantGroups;
cls.NumConstantsInGroups = NumInGroups;
cls.NumConstantsInGroupsForDescendants = NumInDescendantGroups;
-- Remove grouped constants from the normal list:
local NewConstants = {};
for _, cons in ipairs(cls.Constants) do
if (cons.Group == nil) then
table.insert(NewConstants, cons);
end
end
cls.Constants = NewConstants;
end -- if (ConstantGroups ~= nil)
else -- if (APIDesc ~= nil)
-- Class is not documented at all, add all its members to Undocumented lists:
cls.UndocumentedFunctions = {};
cls.UndocumentedConstants = {};
cls.UndocumentedVariables = {};
cls.Variables = cls.Variables or {};
g_Stats.NumUndocumentedClasses = g_Stats.NumUndocumentedClasses + 1;
for _, func in ipairs(cls.Functions) do
local FnName = func.DocID or func.Name;
if not(IsFunctionIgnored(cls.Name, FnName)) then
table.insert(cls.UndocumentedFunctions, FnName);
end
end -- for j, func - cls.Functions[]
for _, cons in ipairs(cls.Constants) do
if not(IsConstantIgnored(cls.Name .. "." .. cons.Name)) then
table.insert(cls.UndocumentedConstants, cons.Name);
end
end -- for j, cons - cls.Constants[]
for _, var in ipairs(cls.Variables) do
if not(IsConstantIgnored(cls.Name .. "." .. var.Name)) then
table.insert(cls.UndocumentedVariables, var.Name);
end
end -- for j, var - cls.Variables[]
end -- else if (APIDesc ~= nil)
-- Remove ignored functions:
local NewFunctions = {};
for _, fn in ipairs(cls.Functions) do
if (not(IsFunctionIgnored(cls.Name, fn.Name))) then
table.insert(NewFunctions, fn);
end
end -- for j, fn
cls.Functions = NewFunctions;
-- Sort the functions (they may have been renamed):
table.sort(cls.Functions,
function(f1, f2)
if (f1.Name == f2.Name) then
-- Same name, either comparing the same function to itself, or two overloads, in which case compare the params
if ((f1.Params == nil) or (f2.Params == nil)) then
return 0;
end
return (f1.Params < f2.Params);
end
return (f1.Name < f2.Name);
end
);
-- Remove ignored constants:
local NewConstants = {};
for _, cn in ipairs(cls.Constants) do
if (not(IsFunctionIgnored(cls.Name, cn.Name))) then
table.insert(NewConstants, cn);
end
end -- for j, cn
cls.Constants = NewConstants;
-- Sort the constants:
table.sort(cls.Constants,
function(c1, c2)
return (c1.Name < c2.Name);
end
);
-- Remove ignored member variables:
local NewVariables = {};
for _, var in ipairs(cls.Variables) do
if (not(IsVariableIgnored(cls.Name .. "." .. var.Name))) then
table.insert(NewVariables, var);
end
end -- for j, var
cls.Variables = NewVariables;
-- Sort the member variables:
table.sort(cls.Variables,
function(v1, v2)
return (v1.Name < v2.Name);
end
);
end -- for i, cls
-- Sort the descendants lists:
for _, cls in ipairs(a_API) do
table.sort(cls.Descendants,
function(c1, c2)
return (c1.Name < c2.Name);
end
);
end -- for i, cls
end
local function ReadHooks(a_Hooks)
--[[
a_Hooks = {
{ Name = "HOOK_1"},
{ Name = "HOOK_2"},
...
};
We want to add hook descriptions to each hook in this array
--]]
for _, hook in ipairs(a_Hooks) do
local HookDesc = g_APIDesc.Hooks[hook.Name];
if (HookDesc ~= nil) then
for key, val in pairs(HookDesc) do
hook[key] = val;
end
end
end -- for i, hook - a_Hooks[]
g_Stats.NumTotalHooks = #a_Hooks;
end
local function WriteHtmlClass(a_ClassAPI, a_ClassMenu)
local cf, err = io.open("API/" .. a_ClassAPI.Name .. ".html", "w");
if (cf == nil) then
LOGINFO("Cannot write HTML API for class " .. a_ClassAPI.Name .. ": " .. err)
return;
end
-- Writes a table containing all functions in the specified list, with an optional "inherited from" header when a_InheritedName is valid
local function WriteFunctions(a_Functions, a_InheritedName)
if (#a_Functions == 0) then
return;
end
if (a_InheritedName ~= nil) then
cf:write("
Functions inherited from ", a_InheritedName, "
\n");
end
cf:write("
\n
Name
Parameters
Return value
Notes
\n");
for _, func in ipairs(a_Functions) do
cf:write("
", func.Name, "
\n");
cf:write("
", LinkifyString(func.Params or "", (a_InheritedName or a_ClassAPI.Name)), "
\n");
cf:write("
", LinkifyString(func.Return or "", (a_InheritedName or a_ClassAPI.Name)), "
\n");
cf:write("
", LinkifyString(func.Notes or "(undocumented)", (a_InheritedName or a_ClassAPI.Name)), "
\n");
end
cf:write("
\n");
end
local function WriteConstantTable(a_Constants, a_Source)
cf:write("
\n
Name
Value
Notes
\n");
for _, cons in ipairs(a_Constants) do
cf:write("
", cons.Name, "
\n");
cf:write("
", cons.Value, "
\n");
cf:write("
", LinkifyString(cons.Notes or "", a_Source), "
\n");
end
cf:write("
\n\n");
end
local function WriteConstants(a_Constants, a_ConstantGroups, a_NumConstantGroups, a_InheritedName)
if ((#a_Constants == 0) and (a_NumConstantGroups == 0)) then
return;
end
local Source = a_ClassAPI.Name
if (a_InheritedName ~= nil) then
cf:write("
Constants inherited from ", a_InheritedName, "
\n");
Source = a_InheritedName;
end
if (#a_Constants > 0) then
WriteConstantTable(a_Constants, Source);
end
for _, group in pairs(a_ConstantGroups) do
if ((a_InheritedName == nil) or group.ShowInDescendants) then
cf:write("
");
end
end
end
local function WriteVariables(a_Variables, a_InheritedName)
if (#a_Variables == 0) then
return;
end
if (a_InheritedName ~= nil) then
cf:write("
Member variables inherited from ", a_InheritedName, "
\n");
end
cf:write("
Name
Type
Notes
\n");
for _, var in ipairs(a_Variables) do
cf:write("
", var.Name, "
\n");
cf:write("
", LinkifyString(var.Type or "(undocumented)", a_InheritedName or a_ClassAPI.Name), "
\n");
cf:write("
", LinkifyString(var.Notes or "", a_InheritedName or a_ClassAPI.Name), "
\n
\n");
end
cf:write("
\n\n");
end
local function WriteDescendants(a_Descendants)
if (#a_Descendants == 0) then
return;
end
cf:write("
");
for _, desc in ipairs(a_Descendants) do
cf:write("
\n");
end
local ClassName = a_ClassAPI.Name;
-- Build an array of inherited classes chain:
local InheritanceChain = {};
local CurrInheritance = a_ClassAPI.Inherits;
while (CurrInheritance ~= nil) do
table.insert(InheritanceChain, CurrInheritance);
CurrInheritance = CurrInheritance.Inherits;
end
cf:write([[
MCServer API - ]], a_ClassAPI.Name, [[ Class
]]);
local HasInheritance = ((#a_ClassAPI.Descendants > 0) or (a_ClassAPI.Inherits ~= nil));
local HasConstants = (#a_ClassAPI.Constants > 0) or (a_ClassAPI.NumConstantsInGroups > 0);
local HasFunctions = (#a_ClassAPI.Functions > 0);
local HasVariables = (#a_ClassAPI.Variables > 0);
for _, cls in ipairs(InheritanceChain) do
HasConstants = HasConstants or (#cls.Constants > 0) or (cls.NumConstantsInGroupsForDescendants > 0);
HasFunctions = HasFunctions or (#cls.Functions > 0);
HasVariables = HasVariables or (#cls.Variables > 0);
end
-- Write the table of contents:
if (HasInheritance) then
cf:write("
\n");
WriteConstants(a_ClassAPI.Constants, a_ClassAPI.ConstantGroups, a_ClassAPI.NumConstantsInGroups, nil);
g_Stats.NumTotalConstants = g_Stats.NumTotalConstants + #a_ClassAPI.Constants + (a_ClassAPI.NumConstantsInGroups or 0);
for _, cls in ipairs(InheritanceChain) do
WriteConstants(cls.Constants, cls.ConstantGroups, cls.NumConstantsInGroupsForDescendants, cls.Name);
end;
end;
-- Write the member variables:
if (HasVariables) then
cf:write("
\n");
WriteVariables(a_ClassAPI.Variables, nil);
g_Stats.NumTotalVariables = g_Stats.NumTotalVariables + #a_ClassAPI.Variables;
for _, cls in ipairs(InheritanceChain) do
WriteVariables(cls.Variables, cls.Name);
end;
end
-- Write the functions, including the inherited ones:
if (HasFunctions) then
cf:write("
\n");
WriteFunctions(a_ClassAPI.Functions, nil);
g_Stats.NumTotalFunctions = g_Stats.NumTotalFunctions + #a_ClassAPI.Functions;
for _, cls in ipairs(InheritanceChain) do
WriteFunctions(cls.Functions, cls.Name);
end
end
-- Write the additional infos:
if (a_ClassAPI.AdditionalInfo ~= nil) then
for i, additional in ipairs(a_ClassAPI.AdditionalInfo) do
cf:write("
\n");
WriteHtmlClass(cls, a_ClassMenu);
end
f:write([[
]]);
end
--- Writes a list of undocumented objects into a file
local function ListUndocumentedObjects(API, UndocumentedHooks)
f = io.open("API/_undocumented.lua", "w");
if (f ~= nil) then
f:write("\n-- This is the list of undocumented API objects, automatically generated by APIDump\n\n");
f:write("g_APIDesc =\n{\n\tClasses =\n\t{\n");
for _, cls in ipairs(API) do
local HasFunctions = ((cls.UndocumentedFunctions ~= nil) and (#cls.UndocumentedFunctions > 0));
local HasConstants = ((cls.UndocumentedConstants ~= nil) and (#cls.UndocumentedConstants > 0));
local HasVariables = ((cls.UndocumentedVariables ~= nil) and (#cls.UndocumentedVariables > 0));
g_Stats.NumUndocumentedFunctions = g_Stats.NumUndocumentedFunctions + #cls.UndocumentedFunctions;
g_Stats.NumUndocumentedConstants = g_Stats.NumUndocumentedConstants + #cls.UndocumentedConstants;
g_Stats.NumUndocumentedVariables = g_Stats.NumUndocumentedVariables + #cls.UndocumentedVariables;
if (HasFunctions or HasConstants or HasVariables) then
f:write("\t\t" .. cls.Name .. " =\n\t\t{\n");
if ((cls.Desc == nil) or (cls.Desc == "")) then
f:write("\t\t\tDesc = \"\"\n");
end
end
if (HasFunctions) then
f:write("\t\t\tFunctions =\n\t\t\t{\n");
table.sort(cls.UndocumentedFunctions);
for _, fn in ipairs(cls.UndocumentedFunctions) do
f:write("\t\t\t\t" .. fn .. " = { Params = \"\", Return = \"\", Notes = \"\" },\n");
end -- for j, fn - cls.UndocumentedFunctions[]
f:write("\t\t\t},\n\n");
end
if (HasConstants) then
f:write("\t\t\tConstants =\n\t\t\t{\n");
table.sort(cls.UndocumentedConstants);
for _, cn in ipairs(cls.UndocumentedConstants) do
f:write("\t\t\t\t" .. cn .. " = { Notes = \"\" },\n");
end -- for j, fn - cls.UndocumentedConstants[]
f:write("\t\t\t},\n\n");
end
if (HasVariables) then
f:write("\t\t\tVariables =\n\t\t\t{\n");
table.sort(cls.UndocumentedVariables);
for _, vn in ipairs(cls.UndocumentedVariables) do
f:write("\t\t\t\t" .. vn .. " = { Type = \"\", Notes = \"\" },\n");
end -- for j, fn - cls.UndocumentedVariables[]
f:write("\t\t\t},\n\n");
end
if (HasFunctions or HasConstants or HasVariables) then
f:write("\t\t},\n\n");
end
end -- for i, cls - API[]
f:write("\t},\n");
if (#UndocumentedHooks > 0) then
f:write("\n\tHooks =\n\t{\n");
for i, hook in ipairs(UndocumentedHooks) do
if (i > 1) then
f:write("\n");
end
f:write("\t\t" .. hook .. " =\n\t\t{\n");
f:write("\t\t\tCalledWhen = \"\",\n");
f:write("\t\t\tDefaultFnName = \"On\", -- also used as pagename\n");
f:write("\t\t\tDesc = [[\n\t\t\t\t\n\t\t\t]],\n");
f:write("\t\t\tParams =\n\t\t\t{\n");
f:write("\t\t\t\t{ Name = \"\", Type = \"\", Notes = \"\" },\n");
f:write("\t\t\t\t{ Name = \"\", Type = \"\", Notes = \"\" },\n");
f:write("\t\t\t\t{ Name = \"\", Type = \"\", Notes = \"\" },\n");
f:write("\t\t\t\t{ Name = \"\", Type = \"\", Notes = \"\" },\n");
f:write("\t\t\t\t{ Name = \"\", Type = \"\", Notes = \"\" },\n");
f:write("\t\t\t\t{ Name = \"\", Type = \"\", Notes = \"\" },\n");
f:write("\t\t\t\t{ Name = \"\", Type = \"\", Notes = \"\" },\n");
f:write("\t\t\t\t{ Name = \"\", Type = \"\", Notes = \"\" },\n");
f:write("\t\t\t},\n");
f:write("\t\t\tReturns = [[\n\t\t\t\t\n\t\t\t]],\n");
f:write("\t\t}, -- " .. hook .. "\n");
end
f:write("\t},\n");
end
f:write("}\n\n\n\n");
f:close();
end
g_Stats.NumUndocumentedHooks = #UndocumentedHooks;
end
--- Lists the API objects that are documented but not available in the API:
local function ListUnexportedObjects()
f = io.open("API/_unexported-documented.txt", "w");
if (f ~= nil) then
for clsname, cls in pairs(g_APIDesc.Classes) do
if not(cls.IsExported) then
-- The whole class is not exported
f:write("class\t" .. clsname .. "\n");
else
if (cls.Functions ~= nil) then
for fnname, fnapi in pairs(cls.Functions) do
if not(fnapi.IsExported) then
f:write("func\t" .. clsname .. "." .. fnname .. "\n");
end
end -- for j, fn - cls.Functions[]
end
if (cls.Constants ~= nil) then
for cnname, cnapi in pairs(cls.Constants) do
if not(cnapi.IsExported) then
f:write("const\t" .. clsname .. "." .. cnname .. "\n");
end
end -- for j, fn - cls.Functions[]
end
end
end -- for i, cls - g_APIDesc.Classes[]
f:close();
end
end
local function ListMissingPages()
local MissingPages = {};
local NumLinks = 0;
for PageName, Referrers in pairs(g_TrackedPages) do
NumLinks = NumLinks + 1;
if not(cFile:Exists("API/" .. PageName .. ".html")) then
table.insert(MissingPages, {Name = PageName, Refs = Referrers} );
end
end;
g_Stats.NumTrackedLinks = NumLinks;
g_TrackedPages = {};
if (#MissingPages == 0) then
-- No missing pages, congratulations!
return;
end
-- Sort the pages by name:
table.sort(MissingPages,
function (Page1, Page2)
return (Page1.Name < Page2.Name);
end
);
-- Output the pages:
local f, err = io.open("API/_missingPages.txt", "w");
if (f == nil) then
LOGWARNING("Cannot open _missingPages.txt for writing: '" .. err .. "'. There are " .. #MissingPages .. " pages missing.");
return;
end
for _, pg in ipairs(MissingPages) do
f:write(pg.Name .. ":\n");
-- Sort and output the referrers:
table.sort(pg.Refs);
f:write("\t" .. table.concat(pg.Refs, "\n\t"));
f:write("\n\n");
end
f:close();
g_Stats.NumInvalidLinks = #MissingPages;
end
--- Writes the documentation statistics (in g_Stats) into the given HTML file
local function WriteStats(f)
local function ExportMeter(a_Percent)
local Color;
if (a_Percent > 99) then
Color = "green";
elseif (a_Percent > 50) then
Color = "orange";
else
Color = "red";
end
local meter = {
"\n",
"
There are ]], g_Stats.NumTrackedLinks, " internal links, ", g_Stats.NumInvalidLinks, " of them are invalid.
"
);
end
local function DumpAPIHtml(a_API)
LOG("Dumping all available functions and constants to API subfolder...");
-- Create the output folder
if not(cFile:IsFolder("API")) then
cFile:CreateFolder("API");
end
LOG("Copying static files..");
cFile:CreateFolder("API/Static");
local localFolder = g_Plugin:GetLocalFolder();
for _, fnam in ipairs(cFile:GetFolderContents(localFolder .. "/Static")) do
cFile:Delete("API/Static/" .. fnam);
cFile:Copy(localFolder .. "/Static/" .. fnam, "API/Static/" .. fnam);
end
-- Extract hook constants:
local Hooks = {};
local UndocumentedHooks = {};
for name, obj in pairs(cPluginManager) do
if (
(type(obj) == "number") and
name:match("HOOK_.*") and
(name ~= "HOOK_MAX") and
(name ~= "HOOK_NUM_HOOKS")
) then
table.insert(Hooks, { Name = name });
end
end
table.sort(Hooks,
function(Hook1, Hook2)
return (Hook1.Name < Hook2.Name);
end
);
ReadHooks(Hooks);
-- Create a "class index" file, write each class as a link to that file,
-- then dump class contents into class-specific file
LOG("Writing HTML files...");
local f, err = io.open("API/index.html", "w");
if (f == nil) then
LOGINFO("Cannot output HTML API: " .. err);
return;
end
-- Create a class navigation menu that will be inserted into each class file for faster navigation (#403)
local ClassMenuTab = {};
for _, cls in ipairs(a_API) do
table.insert(ClassMenuTab, "");
table.insert(ClassMenuTab, cls.Name);
table.insert(ClassMenuTab, " ");
end
local ClassMenu = table.concat(ClassMenuTab, "");
-- Create a hook navigation menu that will be inserted into each hook file for faster navigation(#403)
local HookNavTab = {};
for _, hook in ipairs(Hooks) do
table.insert(HookNavTab, "");
table.insert(HookNavTab, (hook.Name:gsub("^HOOK_", ""))); -- remove the "HOOK_" part of the name
table.insert(HookNavTab, " ");
end
local HookNav = table.concat(HookNavTab, "");
-- Write the HTML file:
f:write([[
MCServer API - Index
MCServer API - Index
The API reference is divided into the following sections:
]]);
WriteArticles(f);
WriteClasses(f, a_API, ClassMenu);
WriteHooks(f, Hooks, UndocumentedHooks, HookNav);
-- Copy the static files to the output folder:
local StaticFiles =
{
"main.css",
"prettify.js",
"prettify.css",
"lang-lua.js",
};
for _, fnam in ipairs(StaticFiles) do
cFile:Delete("API/" .. fnam);
cFile:Copy(g_Plugin:GetLocalFolder() .. "/" .. fnam, "API/" .. fnam);
end
-- List the documentation problems:
LOG("Listing leftovers...");
ListUndocumentedObjects(a_API, UndocumentedHooks);
ListUnexportedObjects();
ListMissingPages();
WriteStats(f);
f:write([[
]]);
f:close();
LOG("API subfolder written");
end
--- Returns the string with extra tabs and CR/LFs removed
local function CleanUpDescription(a_Desc)
-- Get rid of indent and newlines, normalize whitespace:
local res = a_Desc:gsub("[\n\t]", "")
res = a_Desc:gsub("%s%s+", " ")
-- Replace paragraph marks with newlines:
res = res:gsub("
", "\n")
res = res:gsub("
", "")
-- Replace list items with dashes:
res = res:gsub("?ul>", "")
res = res:gsub("
", "\n - ")
res = res:gsub("
", "")
return res
end
--- Writes a list of methods into the specified file in ZBS format
local function WriteZBSMethods(f, a_Methods)
for _, func in ipairs(a_Methods or {}) do
f:write("\t\t\t[\"", func.Name, "\"] =\n")
f:write("\t\t\t{\n")
f:write("\t\t\t\ttype = \"method\",\n")
if ((func.Notes ~= nil) and (func.Notes ~= "")) then
f:write("\t\t\t\tdescription = [[", CleanUpDescription(func.Notes or ""), " ]],\n")
end
f:write("\t\t\t},\n")
end
end
--- Writes a list of constants into the specified file in ZBS format
local function WriteZBSConstants(f, a_Constants)
for _, cons in ipairs(a_Constants or {}) do
f:write("\t\t\t[\"", cons.Name, "\"] =\n")
f:write("\t\t\t{\n")
f:write("\t\t\t\ttype = \"value\",\n")
if ((cons.Desc ~= nil) and (cons.Desc ~= "")) then
f:write("\t\t\t\tdescription = [[", CleanUpDescription(cons.Desc or ""), " ]],\n")
end
f:write("\t\t\t},\n")
end
end
--- Writes one MCS class definition into the specified file in ZBS format
local function WriteZBSClass(f, a_Class)
assert(type(a_Class) == "table")
-- Write class header:
f:write("\t", a_Class.Name, " =\n\t{\n")
f:write("\t\ttype = \"class\",\n")
f:write("\t\tdescription = [[", CleanUpDescription(a_Class.Desc or ""), " ]],\n")
f:write("\t\tchilds =\n")
f:write("\t\t{\n")
-- Export methods and constants:
WriteZBSMethods(f, a_Class.Functions)
WriteZBSConstants(f, a_Class.Constants)
-- Finish the class definition:
f:write("\t\t},\n")
f:write("\t},\n\n")
end
--- Dumps the entire API table into a file in the ZBS format
local function DumpAPIZBS(a_API)
LOG("Dumping ZBS API description...")
local f, err = io.open("mcserver_api.lua", "w")
if (f == nil) then
LOG("Cannot open mcserver_lua.lua for writing, ZBS API will not be dumped. " .. err)
return
end
-- Write the file header:
f:write("-- This is a MCServer API file automatically generated by the APIDump plugin\n")
f:write("-- Note that any manual changes will be overwritten by the next dump\n\n")
f:write("return {\n")
-- Export each class except Globals, store those aside:
local Globals
for _, cls in ipairs(a_API) do
if (cls.Name ~= "Globals") then
WriteZBSClass(f, cls)
else
Globals = cls
end
end
-- Export the globals:
if (Globals) then
WriteZBSMethods(f, Globals.Functions)
WriteZBSConstants(f, Globals.Constants)
end
-- Finish the file:
f:write("}\n")
f:close()
LOG("ZBS API dumped...")
end
--- Returns true if a_Descendant is declared to be a (possibly indirect) descendant of a_Base
local function IsDeclaredDescendant(a_DescendantName, a_BaseName, a_API)
-- Check params:
assert(type(a_DescendantName) == "string")
assert(type(a_BaseName) == "string")
assert(type(a_API) == "table")
if not(a_API[a_BaseName]) then
return false
end
assert(type(a_API[a_BaseName]) == "table", "Not a class name: " .. a_BaseName)
assert(type(a_API[a_BaseName].Descendants) == "table")
-- Check direct inheritance:
for _, desc in ipairs(a_API[a_BaseName].Descendants) do
if (desc.Name == a_DescendantName) then
return true
end
end -- for desc - a_BaseName's descendants
-- Check indirect inheritance:
for _, desc in ipairs(a_API[a_BaseName].Descendants) do
if (IsDeclaredDescendant(a_DescendantName, desc.Name, a_API)) then
return true
end
end -- for desc - a_BaseName's descendants
return false
end
--- Checks the specified class' inheritance
-- Reports any problems as new items in the a_Report table
local function CheckClassInheritance(a_Class, a_API, a_Report)
-- Check params:
assert(type(a_Class) == "table")
assert(type(a_API) == "table")
assert(type(a_Report) == "table")
-- Check that the declared descendants are really descendants:
local registry = debug.getregistry()
for _, desc in ipairs(a_Class.Descendants or {}) do
local isParent = false
local parents = registry["tolua_super"][_G[desc.Name]]
if not(parents[a_Class.Name]) then
table.insert(a_Report, desc.Name .. " is not a descendant of " .. a_Class.Name)
end
end -- for desc - a_Class.Descendants[]
-- Check that all inheritance is listed for the class:
local parents = registry["tolua_super"][_G[a_Class.Name]] -- map of "classname" -> true for each class that a_Class inherits
for clsName, isParent in pairs(parents or {}) do
if ((clsName ~= "") and not(clsName:match("const .*"))) then
if not(IsDeclaredDescendant(a_Class.Name, clsName, a_API)) then
table.insert(a_Report, a_Class.Name .. " inherits from " .. clsName .. " but this isn't documented")
end
end
end
end
--- Checks each class's declared inheritance versus the actual inheritance
local function CheckAPIDescendants(a_API)
-- Check each class:
local report = {}
for _, cls in ipairs(a_API) do
if (cls.Name ~= "Globals") then
CheckClassInheritance(cls, a_API, report)
end
end
-- If there's anything to report, output it to a file:
if (report[1] ~= nil) then
LOG("There are inheritance errors in the API description:")
for _, msg in ipairs(report) do
LOG(" " .. msg)
end
local f, err = io.open("API/_inheritance_errors.txt", "w")
if (f == nil) then
LOG("Cannot report inheritance problems to a file: " .. tostring(err))
return
end
f:write(table.concat(report, "\n"))
f:close()
end
end
local function DumpApi()
LOG("Dumping the API...")
-- Load the API descriptions from the Classes and Hooks subfolders:
-- This needs to be done each time the command is invoked because the export modifies the tables' contents
dofile(g_PluginFolder .. "/APIDesc.lua")
if (g_APIDesc.Classes == nil) then
g_APIDesc.Classes = {};
end
if (g_APIDesc.Hooks == nil) then
g_APIDesc.Hooks = {};
end
LoadAPIFiles("/Classes/", g_APIDesc.Classes);
LoadAPIFiles("/Hooks/", g_APIDesc.Hooks);
-- Reset the stats:
g_TrackedPages = {}; -- List of tracked pages, to be checked later whether they exist. Each item is an array of referring pagenames.
g_Stats = -- Statistics about the documentation
{
NumTotalClasses = 0,
NumUndocumentedClasses = 0,
NumTotalFunctions = 0,
NumUndocumentedFunctions = 0,
NumTotalConstants = 0,
NumUndocumentedConstants = 0,
NumTotalVariables = 0,
NumUndocumentedVariables = 0,
NumTotalHooks = 0,
NumUndocumentedHooks = 0,
NumTrackedLinks = 0,
NumInvalidLinks = 0,
}
-- Create the API tables:
local API, Globals = CreateAPITables();
-- Sort the classes by name:
table.sort(API,
function (c1, c2)
return (string.lower(c1.Name) < string.lower(c2.Name));
end
);
g_Stats.NumTotalClasses = #API;
-- Add Globals into the API:
Globals.Name = "Globals";
table.insert(API, Globals);
-- Read in the descriptions:
LOG("Reading descriptions...");
ReadDescriptions(API);
-- Check that the API lists the inheritance properly, report any problems to a file:
CheckAPIDescendants(API)
-- Dump all available API objects in HTML format into a subfolder:
DumpAPIHtml(API);
-- Dump all available API objects in format used by ZeroBraneStudio API descriptions:
DumpAPIZBS(API)
LOG("APIDump finished");
return true
end
local function HandleWebAdminDump(a_Request)
if (a_Request.PostParams["Dump"] ~= nil) then
DumpApi()
end
return
[[
Pressing the button will generate the API dump on the server. Note that this can take some time.
]]
end
local function HandleCmdApi(a_Split)
DumpApi()
return true
end
local function HandleCmdApiShow(a_Split, a_EntireCmd)
os.execute("API" .. cFile:GetPathSeparator() .. "index.html")
return true, "Launching the browser to show the API docs..."
end
function Initialize(Plugin)
g_Plugin = Plugin;
g_PluginFolder = Plugin:GetLocalFolder();
LOG("Initialising " .. Plugin:GetName() .. " v." .. Plugin:GetVersion())
-- Bind a console command to dump the API:
cPluginManager:BindConsoleCommand("api", HandleCmdApi, "Dumps the Lua API docs into the API/ subfolder")
cPluginManager:BindConsoleCommand("apishow", HandleCmdApiShow, "Runs the default browser to show the API docs")
-- Add a WebAdmin tab that has a Dump button
g_Plugin:AddWebTab("APIDump", HandleWebAdminDump)
return true
end