summaryrefslogtreecommitdiffstats
path: root/MCServer/Plugins/InfoDump.lua
blob: 28a17c214d3689772d63f588a4b509a1c0200a0f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
#!/usr/bin/lua

-- InfoDump.lua

--[[
Loads plugins' Info.lua and dumps its g_PluginInfo 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
--]]





-- 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





--- 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}", "[list]"):gsub("{%%/list}", "[/list]");
	a_Str = a_Str:gsub("{%%li}", "[*]"):gsub("{%%/li}", "");
	-- 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) 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





--- 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 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(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





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 DumpAdditionalInfoForum(a_PluginInfo, f)
	local AInfo = a_PluginInfo.AdditionalInfo;
	if ((AInfo == nil) or (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





--- 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) 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 {};
				table.insert(Permission.CommandsAffected, CommandString);
			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(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 DumpPluginInfoForum(a_PluginFolder, a_PluginInfo)
	-- Open the output file:
	local f, msg = io.open(a_PluginInfo.Name .. "_forum.txt", "w");
	if (f == nil) then
		print("\tCannot dump forum info for plugin " .. a_PluginFolder .. ": " .. msg);
		return;
	end

	-- Write the description:
	f:write(ForumizeString(a_PluginInfo.Description), "\n");
	DumpAdditionalInfoForum(a_PluginInfo, f);
	DumpCommandsForum(a_PluginInfo, f);
	DumpPermissionsForum(a_PluginInfo, f);

	f:close();
end





local function DumpPluginInfoGitHub()
	-- TODO
end





--- Tries to load the g_PluginInfo from the plugin's Info.lua file
-- Returns the g_PluginInfo 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);
	cfg();
	if (Sandbox.g_PluginInfo == nil) then
		return nil, "Info.lua doesn't contain the g_PluginInfo declaration";
	end
	return Sandbox.g_PluginInfo;
end





local function ProcessPluginFolder(a_FolderName)
	local PluginInfo, Msg = LoadPluginInfo(a_FolderName);
	if (PluginInfo == nil) then
		if (Msg ~= nil) then
			print("\t" .. Msg);
		end
		return;
	end
	DumpPluginInfoForum(a_FolderName, PluginInfo);
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, http://luarocks.org/en/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 = ...;
if ((Arg1 ~= nil) and (Arg1 ~= "")) then
	-- Called with a plugin folder name, export only that one
	ProcessPluginFolder(Arg1)
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);
					ProcessPluginFolder(fnam);
				end
			end
		end
	end
end
print("Done.");