1 /** 2 Utility types that wrap specific utility functionality, without directly 3 representing data/modeling from Discord. 4 5 TODO: moveme 6 */ 7 module dcord.types.util; 8 9 import dcord.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 conv = delegate that returns an int and accepts a string, for sorting 165 */ 166 void sort(uint column, int delegate(string) conv=null) { 167 if (conv) { 168 this.entries = std.algorithm.sorting.sort!( 169 (a, b) => conv(a[column]) < conv(b[column]) 170 )(this.entries.dup).array; 171 } else { 172 this.entries = std.algorithm.sorting.sort!( 173 (a, b) => a[column] < b[column])(this.entries.dup).array; 174 } 175 } 176 177 /** 178 Set a header row. Will not be sorted or modified. 179 */ 180 void setHeader(string[] args...) { 181 this.header = this.indexSizes(args.dup); 182 } 183 184 /// Resets sizes index for a given row 185 private string[] indexSizes(string[] row) { 186 size_t pos = 0; 187 188 foreach (part; row) { 189 size_t size = to!wstring(part).length; 190 191 if (this.sizes.length <= pos) { 192 this.sizes ~= size; 193 } else if (this.sizes[pos] < size) { 194 this.sizes[pos] = size; 195 } 196 pos++; 197 } 198 return row; 199 } 200 201 /** 202 Add a row to the table. 203 204 Params: 205 args = sorted columns 206 */ 207 void add(string[] args...) { 208 this.entries ~= this.indexSizes(args.dup); 209 } 210 /** 211 Compile an entry. 212 Params: 213 entry = array of strings 214 */ 215 string compileEntry(string[] entry) { 216 size_t pos; 217 string line; 218 219 // If we're wrapped, add left margin deliminator 220 if (this.wrapped) line ~= this.delim; 221 222 foreach (part; entry) { 223 ulong size = to!wstring(part).length; 224 line ~= part ~ " ".replicate(cast(size_t)(this.sizes[pos] - size)) ~ this.delim; 225 pos++; 226 } 227 228 // If we're not wrapped, remove the end deliminator 229 if (!this.wrapped) line = line[0..($ - this.delim.length)]; 230 231 return line; 232 } 233 234 /// Returns all entries in the table (incl. header) 235 string[][] all() { 236 string[][] ents; 237 238 if (this.header.length) { 239 ents ~= this.header; 240 } 241 242 ents ~= this.entries; 243 return ents; 244 } 245 246 /// Appends the output of this table to a message buffer 247 void appendToBuffer(MessageBuffer buffer) { 248 foreach (entry; this.all()) { 249 buffer.append(this.compileEntry(entry)); 250 } 251 } 252 253 /// Appends the output of this table to N many message buffers 254 MessageBuffer[] appendToBuffers(MessageBuffer delegate() createBuffer=null){ 255 MessageBuffer[] bufs = [createBuffer ? createBuffer() : new MessageBuffer]; 256 257 foreach (entry; this.all()) { 258 string compiled = this.compileEntry(entry); 259 260 if (!bufs[$-1].append(compiled)) { 261 bufs ~= createBuffer ? createBuffer() : new MessageBuffer; 262 bufs[$-1].append(compiled); 263 } 264 } 265 266 return bufs; 267 } 268 269 /// Return the header concentated with the entries 270 string[][] iterEntries() { 271 return this.header ~ this.entries; 272 } 273 }