#!/usr/bin/lua
-- InfoDump.lua
--[[
Loads plugins' Info.lua and dumps its g_PluginInfo (or gPluginInfo) into various text formats
This is used for generating plugin documentation for the forum and for GitHub's INFO.md files
This script can be used in two ways:
Executing "lua InfoDump.lua" will go through all subfolders and dump each Info.lua file it can find
Note that this mode of operation requires LuaRocks with LFS installed; instructions are printed
when the prerequisites are not met.
Executing "lua InfoDump.lua PluginName" will load the Info.lua file from PluginName's folder and dump
only that one plugin's documentation. This mode of operation doesn't require LuaRocks
--]]
-- If this file is called using the loadfile function the arg variable isn't filled. We have to do it manualy then.
local arg = arg or {...}
-- Check Lua version. We use 5.1-specific construct when loading the plugin info, 5.2 is not compatible!
if (_VERSION ~= "Lua 5.1") then
print("Unsupported Lua version. This script requires Lua version 5.1, this Lua is version " .. (_VERSION or "<nil>"))
return
end
--- Removes any whitespace at the beginning and end of the string
local function TrimString(a_Str)
return (string.match(a_Str, "^%s*(.-)%s*$"))
end
--- Replaces generic formatting with forum-specific formatting
-- Also removes the single line-ends
local function ForumizeString(a_Str)
assert(type(a_Str) == "string")
-- Remove the indentation, unless in the code tag:
-- Only one code or /code tag per line is supported!
local IsInCode = false
local function RemoveIndentIfNotInCode(s)
if (IsInCode) then
-- we're in code section, check if this line terminates it
IsInCode = (s:find("{%%/code}") ~= nil)
return s .. "\n"
else
-- we're not in code section, check if this line starts it
IsInCode = (s:find("{%%code}") ~= nil)
return s:gsub("^%s*", "") .. "\n"
end
end
a_Str = a_Str:gsub("(.-)\n", RemoveIndentIfNotInCode)
-- Replace multiple line ends with {%p} and single line ends with a space,
-- so that manual word-wrap in the Info.lua file doesn't wrap in the forum.
a_Str = a_Str:gsub("\n\n", "{%%p}")
a_Str = a_Str:gsub("\n", " ")
-- Replace the generic formatting:
a_Str = a_Str:gsub("{%%p}", "\n\n")
a_Str = a_Str:gsub("{%%b}", "[b]"):gsub("{%%/b}", "[/b]")
a_Str = a_Str:gsub("{%%i}", "[i]"):gsub("{%%/i}", "[/i]")
a_Str = a_Str:gsub("{%%list}", "\n[list]"):gsub("{%%/list}", "[/list]")
a_Str = a_Str:gsub("{%%li}", "\n[*]"):gsub("{%%/li}", "\n")
-- Process links: {%a LinkDestination}LinkText{%/a}
a_Str = a_Str:gsub("{%%a%s([^}]*)}([^{]*){%%/a}", "[url=%1]%2[/url]")
-- TODO: Other formatting
return a_Str
end
--- Replaces generic formatting with forum-specific formatting
-- Also removes the single line-ends
local function GithubizeString(a_Str)
assert(type(a_Str) == "string")
-- Remove the indentation, unless in the code tag:
-- Only one code or /code tag per line is supported!
local IsInCode = false
local function RemoveIndentIfNotInCode(s)
if (IsInCode) then
-- we're in code section, check if this line terminates it
IsInCode = (s:find("{%%/code}") ~= nil)
return s .. "\n"
else
-- we're not in code section, check if this line starts it
IsInCode = (s:find("{%%code}") ~= nil)
return s:gsub("^%s*", "") .. "\n"
end
end
a_Str = a_Str:gsub("(.-)\n", RemoveIndentIfNotInCode)
-- Replace multiple line ends with {%p} and single line ends with a space,
-- so that manual word-wrap in the Info.lua file doesn't wrap in the forum.
a_Str = a_Str:gsub("\n\n", "{%%p}")
a_Str = a_Str:gsub("\n", " ")
-- Replace the generic formatting:
a_Str = a_Str:gsub("{%%p}", "\n\n")
a_Str = a_Str:gsub("{%%b}", "**"):gsub("{%%/b}", "**")
a_Str = a_Str:gsub("{%%i}", "*"):gsub("{%%/i}", "*")
a_Str = a_Str:gsub("{%%list}", "\n"):gsub("{%%/list}", "\n")
a_Str = a_Str:gsub("{%%li}", "\n - "):gsub("{%%/li}", "")
-- Process links: {%a LinkDestination}LinkText{%/a}
a_Str = a_Str:gsub("{%%a%s([^}]*)}([^{]*){%%/a}", "[%2](%1)")
-- TODO: Other formatting
return a_Str
end
--- Builds an array of categories, each containing all the commands belonging to the category,
-- and the category description, if available.
-- Returns the array table, each item has the following format:
-- { Name = "CategoryName", Description = "CategoryDescription", Commands = {{CommandString = "/cmd verb", Info = {...}}, ...}}
local function BuildCategories(a_PluginInfo)
-- The returned result
-- This will contain both an array and a dict of the categories, to allow fast search
local res = {}
-- For each command add a reference to it into all of its categories:
local function AddCommands(a_CmdPrefix, a_Commands)
for cmd, info in pairs(a_Commands or {}) do
local NewCmd =
{
CommandString = a_CmdPrefix .. cmd,
Info = info,
}
if ((info.HelpString ~= nil) and (info.HelpString ~= "")) then
-- Add to each specified category:
local Category = info.Category
if (type(Category) == "string") then
Category = {Category}
end
for idx, cat in ipairs(Category or {""}) do
local CatEntry = res[cat]
if (CatEntry == nil) then
-- First time we came across this category, create it:
local NewCat = {Name = cat, Description = "", Commands = {NewCmd}}
table.insert(res, NewCat)
res[cat] = NewCat
else
-- We already have this category, just add the command to its list of commands:
table.insert(CatEntry.Commands, NewCmd)
end
end -- for idx, cat - Category[]
end -- if (HelpString valid)
-- Recurse all subcommands:
if (info.Subcommands ~= nil) then
AddCommands(a_CmdPrefix .. cmd .. " ", info.Subcommands)
end
end -- for cmd, info - a_Commands[]
end -- AddCommands()
AddCommands("", a_PluginInfo.Commands)
-- Assign descriptions to categories:
for name, desc in pairs(a_PluginInfo.Categories or {}) do
local CatEntry = res[name]
if (CatEntry ~= nil) then
-- The result has this category, add the description:
CatEntry.Description = desc.Description
end
end
-- Alpha-sort each category's command list:
for idx, cat in ipairs(res) do
table.sort(cat.Commands,
function (cmd1, cmd2)
return (string.lower(cmd1.CommandString) < string.lower(cmd2.CommandString))
end
)
end
return res
end
--- Returns a string specifying the command.
-- If a_Command is a simple string, returns a_Command colorized to blue
-- If a_Command is a table, expects members Name (full command name) and Params (command parameters),
-- colorizes command name blue and params green
local function GetCommandRefForum(a_Command)
if (type(a_Command) == "string") then
return "[color=blue]" .. a_Command .. "[/color]"
end
return "[color=blue]" .. a_Command.Name .. "[/color] [color=green]" .. (a_Command.Params or "") .. "[/color]"
end
--- Returns a string specifying the command.
-- If a_CommandParams is nil, returns a_CommandName apostrophed
-- If a_CommandParams is a string, apostrophes a_CommandName with a_CommandParams
local function GetCommandRefGithub(a_CommandName, a_CommandParams)
assert(type(a_CommandName) == "string")
if (a_CommandParams == nil) then
return "`" .. a_CommandName .. "`"
end
assert(type(a_CommandParams) == "table")
if ((a_CommandParams.Params == nil) or (a_CommandParams.Params == "")) then
return "`" .. a_CommandName .. "`"
end
assert(type(a_CommandParams.Params) == "string")
return "`" .. a_CommandName .. " " .. a_CommandParams.Params .. "`"
end
--- Writes the specified command detailed help array to the output file, in the forum dump format
local function WriteCommandParameterCombinationsForum(a_CmdString, a_ParameterCombinations, f)
assert(type(a_CmdString) == "string")
assert(type(a_ParameterCombinations) == "table")
assert(f ~= nil)
if (#a_ParameterCombinations == 0) then
-- No explicit parameter combinations to write
return
end
f:write("The following parameter combinations are recognized:\n")
for idx, combination in ipairs(a_ParameterCombinations) do
f:write("[color=blue]", a_CmdString, "[/color] [color=green]", combination.Params or "", "[/color]")
if (combination.Help ~= nil) then
f:write(" - ", ForumizeString(combination.Help))
end
if (combination.Permission ~= nil) then
f:write(" (Requires permission '[color=red]", combination.Permission, "[/color]')")
end
f:write("\n")
end
end
--- Writes the specified command detailed help array to the output file, in the forum dump format
local function WriteCommandParameterCombinationsGithub(a_CmdString, a_ParameterCombinations, f)
assert(type(a_CmdString) == "string")
assert(type(a_ParameterCombinations) == "table")
assert(f ~= nil)
if (#a_ParameterCombinations == 0) then
-- No explicit parameter combinations to write
return
end
f:write("The following parameter combinations are recognized:\n\n")
for idx, combination in ipairs(a_ParameterCombinations) do
f:write(GetCommandRefGithub(a_CmdString, combination))
if (combination.Help ~= nil) then
f:write(" - ", GithubizeString(combination.Help))
end
if (combination.Permission ~= nil) then
f:write(" (Requires permission '**", combination.Permission, "**')")
end
f:write("\n")
end
end
--- Writes all commands in the specified category to the output file, in the forum dump format
local function WriteCommandsCategoryForum(a_Category, f)
-- Write category name:
local CategoryName = a_Category.Name
if (CategoryName == "") then
CategoryName = "General"
end
f:write("\n[size=large]", ForumizeString(a_Category.DisplayName or CategoryName), "[/size]\n")
-- Write description:
if (a_Category.Description ~= "") then
f:write(ForumizeString(a_Category.Description), "\n")
end
-- Write commands:
f:write("\n[list]")
for idx2, cmd in ipairs(a_Category.Commands) do
f:write("\n[b]", cmd.CommandString, "[/b] - ", ForumizeString(cmd.Info.HelpString or "UNDOCUMENTED"), "\n")
if (cmd.Info.Permission ~= nil) then
f:write("Permission required: [color=red]", cmd.Info.Permission, "[/color]\n")
end
if (cmd.Info.DetailedDescription ~= nil) then
f:write(ForumizeString(cmd.Info.DetailedDescription))
end
if (cmd.Info.ParameterCombinations ~= nil) then
WriteCommandParameterCombinationsForum(cmd.CommandString, cmd.Info.ParameterCombinations, f)
end
end
f:write("[/list]\n\n")
end
--- Writes all commands in the specified category to the output file, in the Github dump format
local function WriteCommandsCategoryGithub(a_Category, f)
-- Write category name:
local CategoryName = a_Category.Name
if (CategoryName == "") then
CategoryName = "General"
end
f:write("\n### ", GithubizeString(a_Category.DisplayName or CategoryName), "\n")
-- Write description:
if (a_Category.Description ~= "") then
f:write(GithubizeString(a_Category.Description), "\n\n")
end
f:write("| Command | Permission | Description |\n")
f:write("| ------- | ---------- | ----------- |\n")
-- Write commands:
for idx2, cmd in ipairs(a_Category.Commands) do
f:write("|", cmd.CommandString, " | ", cmd.Info.Permission or "", " | ", GithubizeString(cmd.Info.HelpString or "UNDOCUMENTED"), "|\n")
end
f:write("\n\n")
end
local function DumpCommandsForum(a_PluginInfo, f)
-- Copy all Categories from a dictionary into an array:
local Categories = BuildCategories(a_PluginInfo)
-- Sort the categories by name:
table.sort(Categories,
function(cat1, cat2)
return (string.lower(cat1.Name) < string.lower(cat2.Name))
end
)
if (#Categories == 0) then
return
end
f:write("\n[size=x-large]Commands[/size]\n")
-- Dump per-category commands:
for idx, cat in ipairs(Categories) do
WriteCommandsCategoryForum(cat, f)
end
end
local function DumpCommandsGithub(a_PluginInfo, f)
-- Copy all Categories from a dictionary into an array:
local Categories = BuildCategories(a_PluginInfo)
-- Sort the categories by name:
table.sort(Categories,
function(cat1, cat2)
return (string.lower(cat1.Name) < string.lower(cat2.Name))
end
)
if (#Categories == 0) then
return
end
f:write("\n# Commands\n")
-- Dump per-category commands:
for idx, cat in ipairs(Categories) do
WriteCommandsCategoryGithub(cat, f)
end
end
local function DumpAdditionalInfoForum(a_PluginInfo, f)
local AInfo = a_PluginInfo.AdditionalInfo
if (type(AInfo) ~= "table") then
-- There is no AdditionalInfo in a_PluginInfo
return
end
for idx, info in ipairs(a_PluginInfo.AdditionalInfo) do
if ((info.Title ~= nil) and (info.Contents ~= nil)) then
f:write("\n[size=x-large]", ForumizeString(info.Title), "[/size]\n")
f:write(ForumizeString(info.Contents), "\n")
end
end
end
local function DumpAdditionalInfoGithub(a_PluginInfo, f)
local AInfo = a_PluginInfo.AdditionalInfo
if (type(AInfo) ~= "table") then
-- There is no AdditionalInfo in a_PluginInfo
return
end
for idx, info in ipairs(a_PluginInfo.AdditionalInfo) do
if ((info.Title ~= nil) and (info.Contents ~= nil)) then
f:write("\n# ", GithubizeString(info.Title), "\n")
f:write(GithubizeString(info.Contents), "\n")
end
end
end
--- Collects all permissions mentioned in the info, returns them as a sorted array
-- Each array item is {Name = "PermissionName", Info = { PermissionInfo }}
local function BuildPermissions(a_PluginInfo)
-- Collect all used permissions from Commands, reference the commands that use the permission:
local Permissions = a_PluginInfo.Permissions or {}
local function CollectPermissions(a_CmdPrefix, a_Commands)
for cmd, info in pairs(a_Commands or {}) do
CommandString = a_CmdPrefix .. cmd
if ((info.Permission ~= nil) and (info.Permission ~= "")) then
-- Add the permission to the list of permissions:
local Permission = Permissions[info.Permission] or {}
Permissions[info.Permission] = Permission
-- Add the command to the list of commands using this permission:
Permission.CommandsAffected = Permission.CommandsAffected or {}
-- First, make sure that we don't already have this command in the list,
-- it may have already been present in a_PluginInfo
local NewCommand = true
for _, existCmd in ipairs(Permission.CommandsAffected) do
if CommandString == existCmd then
NewCommand = false
break
end
end
if NewCommand then
table.insert(Permission.CommandsAffected, CommandString)
end
end
-- Process the command param combinations for permissions:
local ParamCombinations = info.ParameterCombinations or {}
for idx, comb in ipairs(ParamCombinations) do
if ((comb.Permission ~= nil) and (comb.Permission ~= "")) then
-- Add the permission to the list of permissions:
local Permission = Permissions[comb.Permission] or {}
Permissions[info.Permission] = Permission
-- Add the command to the list of commands using this permission:
Permission.CommandsAffected = Permission.CommandsAffected or {}
table.insert(Permission.CommandsAffected, {Name = CommandString, Params = comb.Params})
end
end
-- Process subcommands:
if (info.Subcommands ~= nil) then
CollectPermissions(CommandString .. " ", info.Subcommands)
end
end
end
CollectPermissions("", a_PluginInfo.Commands)
-- Copy the list of permissions to an array:
local PermArray = {}
for name, perm in pairs(Permissions) do
table.insert(PermArray, {Name = name, Info = perm})
end
-- Sort the permissions array:
table.sort(PermArray,
function(p1, p2)
return (p1.Name < p2.Name)
end
)
return PermArray
end
local function DumpPermissionsForum(a_PluginInfo, f)
-- Get the processed sorted array of permissions:
local Permissions = BuildPermissions(a_PluginInfo)
if ((Permissions == nil) or (#Permissions <= 0)) then
return
end
-- Dump the permissions:
f:write("\n[size=x-large]Permissions[/size]\n[list]\n")
for idx, perm in ipairs(Permissions) do
f:write(" - [color=red]", perm.Name, "[/color] - ")
f:write(ForumizeString(perm.Info.Description or ""))
local CommandsAffected = perm.Info.CommandsAffected or {}
if (#CommandsAffected > 0) then
f:write("\n[list] Commands affected:\n- ")
local Affects = {}
for idx2, cmd in ipairs(CommandsAffected) do
table.insert(Affects, GetCommandRefForum(cmd))
end
f:write(table.concat(Affects, "\n - "))
f:write("\n[/list]")
end
if (perm.Info.RecommendedGroups ~= nil) then
f:write("\n[list] Recommended groups: ", perm.Info.RecommendedGroups, "[/list]")
end
f:write("\n")
end
f:write("[/list]")
end
local function DumpPermissionsGithub(a_PluginInfo, f)
-- Get the processed sorted array of permissions:
local Permissions = BuildPermissions(a_PluginInfo)
if ((Permissions == nil) or (#Permissions <= 0)) then
return
end
-- Dump the permissions:
f:write("\n# Permissions\n")
f:write("| Permissions | Description | Commands | Recommended groups |\n")
f:write("| ----------- | ----------- | -------- | ------------------ |\n")
for idx, perm in ipairs(Permissions) do
f:write("| ", perm.Name, " | ")
f:write(GithubizeString(perm.Info.Description or ""), " | ")
local CommandsAffected = perm.Info.CommandsAffected or {}
if (#CommandsAffected > 0) then
local Affects = {}
for idx2, cmd in ipairs(CommandsAffected) do
if (type(cmd) == "string") then
table.insert(Affects, GetCommandRefGithub(cmd))
else
table.insert(Affects, GetCommandRefGithub(cmd.Name, cmd))
end
end
f:write(table.concat(Affects, ", "))
end
f:write(" | ")
if (perm.Info.RecommendedGroups ~= nil) then
f:write(perm.Info.RecommendedGroups)
end
f:write(" |\n")
end
end
--- Dumps the forum-format info for the plugin
-- Returns true on success, nil and error message on failure
local function DumpPluginInfoForum(a_PluginFolder, a_PluginInfo)
-- Open the output file:
local f, msg = io.open(a_PluginFolder .. "/forum_info.txt", "w")
if (f == nil) then
return nil, msg
end
-- Write the description:
f:write(ForumizeString(a_PluginInfo.Description), "\n")
DumpAdditionalInfoForum(a_PluginInfo, f)
DumpCommandsForum(a_PluginInfo, f)
DumpPermissionsForum(a_PluginInfo, f)
if (a_PluginInfo.SourceLocation ~= nil) then
f:write("\n[b]Source[/b]: ", a_PluginInfo.SourceLocation, "\n")
end
if (a_PluginInfo.DownloadLocation ~= nil) then
f:write("[b]Download[/b]: ", a_PluginInfo.DownloadLocation)
end
f:close()
return true
end
--- Dumps the README.md file into the plugin's folder with the GitHub Markdown format
-- Returns true on success, nil and error message on failure
local function DumpPluginInfoGithub(a_PluginFolder, a_PluginInfo)
-- Check the params:
assert(type(a_PluginFolder) == "string")
assert(type(a_PluginInfo) == "table")
-- Open the output file:
local f, msg = io.open(a_PluginFolder .. "/README.md", "w")
if (f == nil) then
print("\tCannot dump github info for plugin " .. a_PluginFolder .. ": " .. msg)
return nil, msg
end
-- Write the description:
f:write(TrimString(GithubizeString(a_PluginInfo.Description)), "\n")
DumpAdditionalInfoGithub(a_PluginInfo, f)
DumpCommandsGithub(a_PluginInfo, f)
DumpPermissionsGithub(a_PluginInfo, f)
f:close()
return true
end
--- Tries to load the g_PluginInfo or gPluginInfo from the plugin's Info.lua file
-- Returns the plugin info table on success, or nil and error message on failure
local function LoadPluginInfo(a_FolderName)
-- Load and compile the Info file:
local cfg, err = loadfile(a_FolderName .. "/Info.lua")
if (cfg == nil) then
return nil, "Cannot open 'Info.lua': " .. err
end
-- Execute the loaded file in a sandbox:
-- This is Lua-5.1-specific and won't work in Lua 5.2!
local Sandbox = {}
setfenv(cfg, Sandbox)
local isSuccess, errMsg = pcall(cfg)
if not(isSuccess) then
return nil, "Cannot load Info.lua: " .. (errMsg or "<unknown error>")
end
if (Sandbox.g_PluginInfo) then
return Sandbox.g_PluginInfo
elseif (Sandbox.gPluginInfo) then
return Sandbox.gPluginInfo
end
return nil, "Info.lua doesn't contain the g_PluginInfo declaration"
end
--- Processes the info for one plugin
-- Returns true on success, nil and error message on failure
local function ProcessPluginFolder(a_FolderName)
-- Load the plugin info:
local PluginInfo, Msg = LoadPluginInfo(a_FolderName)
if (PluginInfo == nil) then
return nil, "Cannot load info for plugin " .. a_FolderName .. ": " .. (Msg or "<unknown error>")
end
-- Dump the forum format:
local isSuccess
isSuccess, Msg = DumpPluginInfoForum(a_FolderName, PluginInfo)
if not(isSuccess) then
return nil, "Cannot dump forum info for plugin " .. a_FolderName .. ": " .. (Msg or "<unknown error>")
end
-- Dump the GitHub format:
isSuccess, Msg = DumpPluginInfoGithub(a_FolderName, PluginInfo)
if not(isSuccess) then
return nil, "Cannot dump GitHub info for plugin " .. a_FolderName .. ": " .. (Msg or "<unknown error>")
end
-- All OK, return success
return true
end
--- Tries to load LFS through LuaRocks, returns the LFS instance, or nil on error
local function LoadLFS()
-- Try to load lfs, do not abort if not found ...
local lfs, err = pcall(
function()
return require("lfs")
end
)
-- ... rather, print a nice message with instructions:
if not(lfs) then
print([[
Cannot load LuaFileSystem
Install it through luarocks by executing the following command:
luarocks install luafilesystem (Windows)
sudo luarocks install luafilesystem (*nix)
If you don't have luarocks installed, you need to install them using your OS's package manager, usually:
sudo apt-get install luarocks (Ubuntu / Debian)
On windows, a binary distribution can be downloaded from the LuaRocks homepage, https://github.com/luarocks/luarocks/wiki/Download
]])
print("Original error text: ", err)
return nil
end
-- We now know that LFS is present, get it normally:
return require("lfs")
end
local Arg1 = arg[1]
if ((Arg1 ~= nil) and (Arg1 ~= "")) then
-- Called with a plugin folder name, export only that one
local isSuccess, msg = ProcessPluginFolder(Arg1)
if not(isSuccess) then
print(msg or "<unknown error>")
end
else
-- Called without any arguments, process all subfolders:
local lfs = LoadLFS()
if (lfs == nil) then
-- LFS not loaded, error has already been printed, just bail out
return
end
print("Processing plugin subfolders:")
for fnam in lfs.dir(".") do
if ((fnam ~= ".") and (fnam ~= "..")) then
local Attributes = lfs.attributes(fnam)
if (Attributes ~= nil) then
if (Attributes.mode == "directory") then
print(fnam)
local isSuccess, msg = ProcessPluginFolder(fnam)
if not(isSuccess) then
print(" " .. (msg or "<unknown error>"))
end
end
end
end
end
end
print("Done.")