|
6 | 6 | import dev.plex.request.GetMapping; |
7 | 7 | import jakarta.servlet.http.HttpServletRequest; |
8 | 8 | import jakarta.servlet.http.HttpServletResponse; |
| 9 | + |
9 | 10 | import java.util.ArrayList; |
10 | 11 | import java.util.Comparator; |
11 | 12 | import java.util.List; |
12 | 13 | import java.util.SortedMap; |
13 | 14 | import java.util.TreeMap; |
| 15 | + |
14 | 16 | import org.bukkit.Bukkit; |
15 | 17 | import org.bukkit.command.Command; |
16 | 18 | import org.bukkit.command.CommandMap; |
17 | 19 | import org.bukkit.command.PluginIdentifiableCommand; |
18 | 20 |
|
19 | 21 | public class CommandsEndpoint extends AbstractServlet |
20 | 22 | { |
21 | | - |
22 | | - private final StringBuilder list = new StringBuilder(); |
23 | | - private boolean loadedCommands = false; |
| 23 | + private String cachedHtml; |
24 | 24 |
|
25 | 25 | @GetMapping(endpoint = "/api/commands/") |
26 | 26 | public String getCommands(HttpServletRequest request, HttpServletResponse response) |
27 | 27 | { |
28 | | - if (!loadedCommands) |
| 28 | + if (cachedHtml == null) |
29 | 29 | { |
30 | | - final SortedMap<String, List<Command>> commandMap = new TreeMap<>(); |
31 | | - final CommandMap map = Bukkit.getCommandMap(); |
32 | | - for (Command command : map.getKnownCommands().values()) |
| 30 | + cachedHtml = buildSections(); |
| 31 | + } |
| 32 | + String file = readFile(this.getClass().getResourceAsStream("/httpd/commands.html")); |
| 33 | + file = file.replace("${commands}", cachedHtml); |
| 34 | + return file; |
| 35 | + } |
| 36 | + |
| 37 | + private static String buildSections() |
| 38 | + { |
| 39 | + final SortedMap<String, List<Command>> commandMap = new TreeMap<>(); |
| 40 | + final CommandMap map = Bukkit.getCommandMap(); |
| 41 | + for (Command command : map.getKnownCommands().values()) |
| 42 | + { |
| 43 | + String plugin = "Bukkit"; |
| 44 | + if (command instanceof PluginIdentifiableCommand pic) |
33 | 45 | { |
34 | | - String plugin = "Bukkit"; |
35 | | - if (command instanceof PluginIdentifiableCommand) |
36 | | - { |
37 | | - plugin = ((PluginIdentifiableCommand) command).getPlugin().getName(); |
38 | | - } |
39 | | - |
40 | | - List<Command> pluginCommands = commandMap.computeIfAbsent(plugin, k -> new ArrayList<>()); |
41 | | - if (!pluginCommands.contains(command)) |
42 | | - { |
43 | | - pluginCommands.add(command); |
44 | | - } |
| 46 | + plugin = pic.getPlugin().getName(); |
45 | 47 | } |
46 | | - |
47 | | - for (String key : commandMap.keySet()) |
| 48 | + List<Command> pluginCommands = commandMap.computeIfAbsent(plugin, k -> new ArrayList<>()); |
| 49 | + if (!pluginCommands.contains(command)) |
48 | 50 | { |
49 | | - commandMap.get(key).sort(Comparator.comparing(Command::getName)); |
50 | | - StringBuilder rows = new StringBuilder(); |
51 | | - for (Command command : commandMap.get(key)) |
52 | | - { |
53 | | - String permission = command.getPermission(); |
54 | | - if (command instanceof PlexCommand plexCmd) |
55 | | - { |
56 | | - CommandPermissions perms = plexCmd.getClass().getAnnotation(CommandPermissions.class); |
57 | | - if (perms != null) |
58 | | - { |
59 | | - permission = (perms.permission().isBlank() ? "N/A" : perms.permission()); |
60 | | - } |
61 | | - } |
62 | | - |
63 | | - rows.append(createRow(command.getName(), command.getAliases(), command.getDescription(), command.getUsage(), permission)); |
64 | | - } |
65 | | - |
66 | | - list.append(createTable(key, rows.toString())).append("\n"); |
| 51 | + pluginCommands.add(command); |
67 | 52 | } |
| 53 | + } |
68 | 54 |
|
69 | | - loadedCommands = true; |
| 55 | + StringBuilder sb = new StringBuilder(); |
| 56 | + for (String key : commandMap.keySet()) |
| 57 | + { |
| 58 | + List<Command> commands = commandMap.get(key); |
| 59 | + commands.sort(Comparator.comparing(Command::getName)); |
| 60 | + sb.append(renderSection(key, commands)); |
70 | 61 | } |
| 62 | + return sb.toString(); |
| 63 | + } |
71 | 64 |
|
72 | | - return commandsHTML(list.toString()); |
| 65 | + private static String renderSection(String plugin, List<Command> commands) |
| 66 | + { |
| 67 | + StringBuilder cards = new StringBuilder(); |
| 68 | + for (Command c : commands) |
| 69 | + { |
| 70 | + cards.append(renderCard(c)); |
| 71 | + } |
| 72 | + String name = escapeHtml(plugin); |
| 73 | + return """ |
| 74 | + <details class="command-section group mt-3 first:mt-0" data-plugin="%s" open> |
| 75 | + <summary class="group flex cursor-pointer list-none items-center justify-between gap-3 rounded-2xl px-2 py-3 transition-colors hover:bg-muted/40 [&::-webkit-details-marker]:hidden"> |
| 76 | + <span class="flex items-center gap-2.5 text-lg font-medium tracking-tight"> |
| 77 | + <svg class="size-4 text-muted-foreground transition-transform group-open:rotate-90" aria-hidden="true"><use href="#i-arrow-right"/></svg> |
| 78 | + %s |
| 79 | + </span> |
| 80 | + <span class="font-mono text-[11px] uppercase tracking-wider text-muted-foreground"> |
| 81 | + %d %s |
| 82 | + </span> |
| 83 | + </summary> |
| 84 | + <div class="mt-3 grid gap-3 md:grid-cols-2 xl:grid-cols-3"> |
| 85 | + %s |
| 86 | + </div> |
| 87 | + </details> |
| 88 | + """.formatted(name, name, commands.size(), commands.size() == 1 ? "command" : "commands", cards); |
73 | 89 | } |
74 | 90 |
|
75 | | - private String commandsHTML(String commandsList) |
| 91 | + private static String renderCard(Command c) |
76 | 92 | { |
77 | | - String file = readFile(this.getClass().getResourceAsStream("/httpd/commands.html")); |
78 | | - file = file.replace("${commands}", commandsList); |
79 | | - return file; |
| 93 | + String name = escapeHtml(c.getName()); |
| 94 | + String aliases = c.getAliases() == null || c.getAliases().isEmpty() ? "" : String.join(", ", c.getAliases()); |
| 95 | + String description = c.getDescription() == null || c.getDescription().isBlank() ? "" : escapeHtml(c.getDescription()); |
| 96 | + String usage = cleanUsage(c.getUsage()); |
| 97 | + String permission = resolvePermission(c); |
| 98 | + |
| 99 | + String aliasMarkup = aliases.isEmpty() |
| 100 | + ? "" |
| 101 | + : "<span class=\"font-mono text-xs text-muted-foreground\">/ " + escapeHtml(aliases) + "</span>"; |
| 102 | + |
| 103 | + String descMarkup = description.isEmpty() |
| 104 | + ? "<p class=\"mt-2 text-sm text-muted-foreground/70 italic\">No description provided.</p>" |
| 105 | + : "<p class=\"mt-2 text-sm text-muted-foreground\">" + description + "</p>"; |
| 106 | + |
| 107 | + String searchBlob = (name + " " + aliases + " " + description + " " + permission).toLowerCase(); |
| 108 | + |
| 109 | + return """ |
| 110 | + <article class="ring-card group flex flex-col rounded-2xl bg-card p-4 transition-colors hover:bg-secondary/50" data-search="%s"> |
| 111 | + <header class="flex flex-wrap items-baseline gap-2"> |
| 112 | + <code class="rounded-md bg-muted px-2 py-0.5 font-mono text-sm font-medium text-foreground">/%s</code> |
| 113 | + %s |
| 114 | + </header> |
| 115 | + %s |
| 116 | + <dl class="mt-3 grid grid-cols-[max-content_1fr] gap-x-3 gap-y-1.5 border-t border-border/60 pt-3 font-mono text-[11px]"> |
| 117 | + <dt class="text-muted-foreground uppercase tracking-wider">usage</dt> |
| 118 | + <dd class="text-foreground/80 break-all">%s</dd> |
| 119 | + <dt class="text-muted-foreground uppercase tracking-wider">perm</dt> |
| 120 | + <dd class="text-foreground/80 break-all">%s</dd> |
| 121 | + </dl> |
| 122 | + </article> |
| 123 | + """.formatted(searchBlob, name, aliasMarkup, descMarkup, usage, permission); |
80 | 124 | } |
81 | 125 |
|
82 | | - private String createTable(String pluginName, String commandRows) |
| 126 | + private static String resolvePermission(Command c) |
83 | 127 | { |
84 | | - return "<details id=\"" + pluginName + "\"><summary>" + pluginName + "</summary>\n" |
85 | | - + "<table id=\"" + pluginName + "Table\" class=\"table table-striped table-bordered\">\n" |
86 | | - + " <thead>\n <tr>\n <th scope=\"col\">Name (Aliases)</th>\n " |
87 | | - + "<th scope=\"col\">Description</th>\n " |
88 | | - + "<th scope=\"col\">Usage</th>\n " |
89 | | - + "<th scope=\"col\">Permission</th>\n </tr>\n</thead>\n" |
90 | | - + "<tbody>\n " + commandRows + "\n</tbody>\n</table>\n</details>"; |
| 128 | + String permission = c.getPermission(); |
| 129 | + if (c instanceof PlexCommand plexCmd) |
| 130 | + { |
| 131 | + CommandPermissions perms = plexCmd.getClass().getAnnotation(CommandPermissions.class); |
| 132 | + if (perms != null) |
| 133 | + { |
| 134 | + permission = perms.permission().isBlank() ? "N/A" : perms.permission(); |
| 135 | + } |
| 136 | + } |
| 137 | + if (permission == null || permission.isBlank()) return "N/A"; |
| 138 | + return escapeHtml(permission).replace(";", "<br>"); |
91 | 139 | } |
92 | 140 |
|
93 | | - private String createRow(String name, List<String> aliases, String description, String usage, String permission) |
| 141 | + private static String cleanUsage(String usage) |
94 | 142 | { |
95 | | - return " <tr>\n <th scope=\"row\">" + name |
96 | | - + (aliases.isEmpty() || aliases.toString().equals("[]") ? "" : " (" + String.join(", ", aliases) + ")") + "</th>\n" |
97 | | - + " <th scope=\"row\">" + description + "</th>\n" |
98 | | - + " <th scope=\"row\"><code>" + cleanUsage(usage) + "</code></th>\n" |
99 | | - + " <th scope=\"row\">" + (permission != null ? permission.replaceAll(";", "<br>") : "N/A") + "</th>\n </tr>"; |
| 143 | + if (usage == null || usage.isBlank()) return "Not provided"; |
| 144 | + String escaped = escapeHtml(usage); |
| 145 | + return escaped.startsWith("/") ? escaped : "/" + escaped; |
100 | 146 | } |
101 | 147 |
|
102 | | - private String cleanUsage(String usage) |
| 148 | + private static String escapeHtml(String s) |
103 | 149 | { |
104 | | - usage = usage.replaceAll("<", "<").replaceAll(">", ">"); |
105 | | - if (usage.isBlank()) |
106 | | - { |
107 | | - usage = "Not Provided"; |
108 | | - } |
109 | | - return usage.startsWith("/") || usage.equals("Not Provided") ? usage : "/" + usage; |
| 150 | + if (s == null) return ""; |
| 151 | + return s.replace("&", "&") |
| 152 | + .replace("<", "<") |
| 153 | + .replace(">", ">") |
| 154 | + .replace("\"", """); |
110 | 155 | } |
111 | 156 | } |
0 commit comments