1 /** 2 Utility types that wrap specific utility functionality, without directly 3 representing data/modeling from Discord. 4 5 TODO: moveme 6 */ 7 module dscord.types.util; 8 9 import dscord.types.message; 10 11 import std.format, 12 std.array, 13 std.conv, 14 std.algorithm.sorting; 15 16 /** 17 Utility class for constructing messages that can be sent over discord, allowing 18 for inteligent limiting of size and stripping of formatting characters. 19 */ 20 class MessageBuffer : BaseSendable { 21 private { 22 bool codeBlock; 23 bool filter; 24 size_t _maxLength; 25 string[] lines; 26 } 27 28 /** 29 Params: 30 codeBlock = if true, this message will be sent within a codeblock 31 filter = if true, appended lines will be filtered for codeblocks/newlines 32 maxLength = maximum length of this message, defaults to 2000 33 */ 34 this(bool codeBlock = true, bool filter = true, size_t maxLength = 2000) { 35 this.codeBlock = codeBlock; 36 this.filter = filter; 37 this._maxLength = maxLength; 38 } 39 40 override immutable(string) getContents() { 41 return this.contents; 42 } 43 44 /// Remove the last line in the buffer 45 string popBack() { 46 string line = this.lines[$-1]; 47 this.lines = this.lines[0..$-2]; 48 return line; 49 } 50 51 /// Max length of this message (subtracting for formatting) 52 @property size_t maxLength() { 53 size_t value = this._maxLength; 54 55 // Codeblock backticks 56 if (this.codeBlock) { 57 value -= 6; 58 } 59 60 // Newlines 61 value -= this.lines.length; 62 63 return value; 64 } 65 66 /** 67 Current length of this message (without formatting) 68 */ 69 @property size_t length() { 70 size_t len; 71 72 foreach (line; lines) { 73 len += line.length; 74 } 75 76 return len; 77 } 78 79 /** 80 Formatted contents of this message. 81 */ 82 @property string contents() { 83 string contents = this.lines.join("\n"); 84 85 // Only format as a codeblock if we actually have contents 86 if (this.codeBlock && contents.length) { 87 return "```" ~ contents ~ "```"; 88 } 89 90 return contents; 91 } 92 93 /** 94 Append a line to this message. Returns false if the buffer is full. 95 */ 96 bool append(string line) { 97 string raw = line; 98 99 // TODO: make this smarter 100 if (this.filter) { 101 if (this.codeBlock) { 102 raw = raw.replace("`", ""); 103 } 104 105 raw = raw.replace("\n", ""); 106 } 107 108 if (this.length + raw.length > this.maxLength) { 109 return false; 110 } 111 112 this.lines ~= raw; 113 return true; 114 } 115 116 /** 117 Format and append a line to this message. Returns false if the buffer is 118 full. 119 */ 120 bool appendf(T...)(string fmt, T args) { 121 return this.append(format(fmt, args)); 122 } 123 } 124 125 /** 126 Utility class for constructing tabulated messages. 127 */ 128 class MessageTable : BaseSendable { 129 private { 130 string[] header; 131 string[][] entries; 132 size_t[] sizes; 133 string delim; 134 bool wrapped; 135 } 136 137 // Message buffer used to compile the message 138 MessageBuffer buffer; 139 140 /** 141 Creates a new MessageTable 142 143 Params: 144 delim = deliminator to use between table columns 145 wrapped = whether to place a deliminator at the left/right margins 146 */ 147 this(string delim=" | ", bool wrapped=true, MessageBuffer buffer=null) { 148 this.delim = delim; 149 this.wrapped = wrapped; 150 this.buffer = buffer; 151 } 152 153 override immutable(string) getContents() { 154 if (!this.buffer) this.buffer = new MessageBuffer; 155 if (!this.buffer.length) this.appendToBuffer(this.buffer); 156 return buffer.getContents(); 157 } 158 159 /** 160 Sort entries by column. Column must be integral. 161 162 Params: 163 column = column index to sort by (0 based) 164 */ 165 void sort(uint column, int delegate(string) conv=null) { 166 if (conv) { 167 this.entries = std.algorithm.sorting.sort!( 168 (a, b) => conv(a[column]) < conv(b[column]) 169 )(this.entries.dup).array; 170 } else { 171 this.entries = std.algorithm.sorting.sort!( 172 (a, b) => a[column] < b[column])(this.entries.dup).array; 173 } 174 } 175 176 /** 177 Set a header row. Will not be sorted or modified. 178 */ 179 void setHeader(string[] args...) { 180 this.header = this.indexSizes(args.dup); 181 } 182 183 /// Resets sizes index for a given row 184 private string[] indexSizes(string[] row) { 185 size_t pos = 0; 186 187 foreach (part; row) { 188 size_t size = to!wstring(part).length; 189 190 if (this.sizes.length <= pos) { 191 this.sizes ~= size; 192 } else if (this.sizes[pos] < size) { 193 this.sizes[pos] = size; 194 } 195 pos++; 196 } 197 return row; 198 } 199 200 /** 201 Add a row to the table. 202 203 Params: 204 args = sorted columns 205 */ 206 void add(string[] args...) { 207 this.entries ~= this.indexSizes(args.dup); 208 } 209 210 string compileEntry(string[] entry) { 211 size_t pos; 212 string line; 213 214 // If we're wrapped, add left margin deliminator 215 if (this.wrapped) line ~= this.delim; 216 217 foreach (part; entry) { 218 ulong size = to!wstring(part).length; 219 line ~= part ~ " ".replicate(cast(size_t)(this.sizes[pos] - size)) ~ this.delim; 220 pos++; 221 } 222 223 // If we're not wrapped, remove the end deliminator 224 if (!this.wrapped) line = line[0..($ - this.delim.length)]; 225 226 return line; 227 } 228 229 /// Returns all entries in the table (incl. header) 230 string[][] all() { 231 string[][] ents; 232 233 if (this.header.length) { 234 ents ~= this.header; 235 } 236 237 ents ~= this.entries; 238 return ents; 239 } 240 241 /// Appends the output of this table to a message buffer 242 void appendToBuffer(MessageBuffer buffer) { 243 foreach (entry; this.all()) { 244 buffer.append(this.compileEntry(entry)); 245 } 246 } 247 248 /// Appends the output of this table to N many message buffers 249 MessageBuffer[] appendToBuffers(MessageBuffer delegate() createBuffer=null){ 250 MessageBuffer[] bufs = [createBuffer ? createBuffer() : new MessageBuffer]; 251 252 foreach (entry; this.all()) { 253 string compiled = this.compileEntry(entry); 254 255 if (!bufs[$-1].append(compiled)) { 256 bufs ~= createBuffer ? createBuffer() : new MessageBuffer; 257 bufs[$-1].append(compiled); 258 } 259 } 260 261 return bufs; 262 } 263 264 string[][] iterEntries() { 265 return this.header ~ this.entries; 266 } 267 }