1 /* 2 Copyright (c) 2009 Simon Veith <simon@jinfinote.com> 3 4 Permission is hereby granted, free of charge, to any person obtaining a copy 5 of this software and associated documentation files (the "Software"), to deal 6 in the Software without restriction, including without limitation the rights 7 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 copies of the Software, and to permit persons to whom the Software is 9 furnished to do so, subject to the following conditions: 10 11 The above copyright notice and this permission notice shall be included in 12 all copies or substantial portions of the Software. 13 14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 THE SOFTWARE. 21 */ 22 23 /** Creates a new Segment instance given a user ID and a string. 24 * @param {Number} user User ID 25 * @param {String} text Text 26 * @class Stores a chunk of text together with the user it was written by. 27 */ 28 function Segment(user, text) { 29 this.user = user; 30 this.text = text; 31 } 32 33 Segment.prototype.toString = function() { 34 return this.text; 35 }; 36 37 Segment.prototype.toHTML = function() { 38 var text = this.text 39 .replace("<", "<") 40 .replace(">", ">") 41 .replace("&", "&") 42 ; 43 44 return '<span class="segment user-' + this.user + '">' + text + '</span>'; 45 }; 46 47 /** Creates a copy of this segment. 48 * @returns {Segment} A copy of this segment. 49 */ 50 Segment.prototype.copy = function() { 51 return new Segment(this.user, this.text) 52 }; 53 54 /** 55 * Creates a new Buffer instance from the given array of 56 * segments. 57 * @param {Array} [segments] The segments that this buffer should be 58 * pre-filled with. 59 * @class Holds multiple Segments and provides methods for modifying them at 60 * a character level. 61 */ 62 function Buffer(segments) { 63 this.segments = new Array(); 64 65 if(segments && segments.length) 66 { 67 for(var index in segments) 68 this.segments.push(segments[index].copy()); 69 } 70 } 71 72 Buffer.prototype.toString = function() { return this.segments.join(""); }; 73 74 Buffer.prototype.toHTML = function() { 75 var result = '<span class="buffer">'; 76 for(var index = 0; index < this.segments.length; index++) 77 result += this.segments[index].toHTML(); 78 result += '</span>'; 79 return result; 80 }; 81 82 /** Creates a deep copy of this buffer. 83 * @type Buffer 84 */ 85 Buffer.prototype.copy = function() { 86 return this.slice(0); 87 }; 88 89 /** Cleans up the buffer by removing empty segments and combining adjacent 90 * segments by the same user. 91 */ 92 Buffer.prototype.compact = function() { 93 var segmentIndex = 0; 94 while(segmentIndex < this.segments.length) 95 { 96 if(this.segments[segmentIndex].text.length == 0) 97 { 98 // This segment is empty, remove it. 99 this.segments.splice(segmentIndex, 1); 100 continue; 101 } else if(segmentIndex < this.segments.length - 1 && 102 this.segments[segmentIndex].user == 103 this.segments[segmentIndex+1].user) { 104 105 // Two consecutive segments are from the same user; merge them 106 // into one. 107 this.segments[segmentIndex].text += 108 this.segments[segmentIndex+1].text; 109 110 this.segments.splice(segmentIndex+1, 1); 111 continue; 112 } 113 114 segmentIndex += 1; 115 } 116 }; 117 118 /** Calculates the total number of characters contained in this buffer. 119 * @returns Total character count in this buffer 120 * @type Number 121 */ 122 Buffer.prototype.getLength = function() { 123 var length = 0; 124 for(var index = 0; index < this.segments.length; index++) 125 length += this.segments[index].text.length; 126 127 return length; 128 } 129 130 /** Extracts a deep copy of a range of characters in this buffer and returns 131 * it as a new Buffer object. 132 * @param {Number} begin Index of first character to return 133 * @param {Number} [end] Index of last character (exclusive). If not 134 * provided, defaults to the total length of the buffer. 135 * @returns New buffer containing the specified character range. 136 * @type Buffer 137 */ 138 Buffer.prototype.slice = function(begin, end) { 139 var result = new Buffer(); 140 141 var segmentIndex = 0, segmentOffset = 0, sliceBegin = begin, 142 sliceEnd = end; 143 144 if(sliceEnd == undefined) 145 sliceEnd = Number.MAX_VALUE; 146 147 while(segmentIndex < this.segments.length && sliceEnd >= segmentOffset) 148 { 149 var segment = this.segments[segmentIndex]; 150 if(sliceBegin - segmentOffset < segment.text.length && 151 sliceEnd - segmentOffset > 0) 152 { 153 var newText = segment.text.slice(sliceBegin - segmentOffset, 154 sliceEnd - segmentOffset); 155 var newSegment = new Segment(segment.user, newText); 156 result.segments.push(newSegment); 157 158 sliceBegin += newText.length; 159 } 160 161 segmentOffset += segment.text.length; 162 segmentIndex += 1; 163 } 164 165 result.compact(); 166 167 return result; 168 } 169 170 /** 171 * Like the Array "splice" method, this method allows for removing and 172 * inserting text in a buffer at a character level. 173 * @param {Number} index The offset at which to begin inserting/removing 174 * @param {Number} [remove] Number of characters to remove 175 * @param {Buffer} [insert] Buffer to insert 176 */ 177 Buffer.prototype.splice = function(index, remove, insert) { 178 if(index > this.getLength()) 179 throw "Buffer splice operation out of bounds"; 180 181 var segmentIndex = 0, segmentOffset = 0, spliceIndex = index, 182 spliceCount = remove, spliceInsertOffset = undefined; 183 while(segmentIndex < this.segments.length) 184 { 185 var segment = this.segments[segmentIndex]; 186 187 if(spliceIndex >= 0 && spliceIndex < segment.text.length) 188 { 189 // This segment is part of the region to splice. 190 191 // Store the text that this splice operation removes to adjust the 192 // splice offset correctly later on. 193 var removedText = segment.text.slice(spliceIndex, spliceIndex + 194 spliceCount); 195 196 if(spliceIndex == 0) { 197 // abcdefg 198 // ^ We're splicing at the beginning of a segment 199 200 if(spliceIndex + spliceCount < segment.text.length) 201 { 202 // abcdefg 203 // ^---^ Remove a part at the beginning 204 205 if(spliceInsertOffset == undefined) 206 spliceInsertOffset = segmentIndex; 207 208 segment.text = segment.text.slice(spliceIndex + 209 spliceCount); 210 } else { 211 // abcdefg 212 // ^-----^ Remove the entire segment 213 214 if(spliceInsertOffset == undefined) 215 spliceInsertOffset = segmentIndex; 216 217 segment.text = ""; 218 this.segments.splice(segmentIndex, 1); 219 segmentIndex -= 1; 220 } 221 } else { 222 // abcdefg 223 // ^ We're splicing inside a segment 224 225 if(spliceInsertOffset == undefined) 226 spliceInsertOffset = segmentIndex + 1; 227 228 if(spliceIndex + spliceCount < segment.text.length) 229 { 230 // abcdefg 231 // ^--^ Remove a part in between 232 233 // Note that if spliceCount == 0, this function only 234 // splits the segment in two. This is necessary in case we 235 // want to insert new segments later. 236 237 var splicePost = new Segment(segment.user, 238 segment.text.slice(spliceIndex + spliceCount)); 239 segment.text = segment.text.slice(0, spliceIndex); 240 this.segments.splice(segmentIndex + 1, 0, splicePost); 241 } else { 242 // abcdefg 243 // ^---^ Remove a part at the end 244 245 segment.text = segment.text.slice(0, spliceIndex); 246 } 247 } 248 249 spliceCount -= removedText.length; 250 } 251 252 if(spliceIndex < segment.text.length && spliceCount == 0) 253 { 254 // We have removed the specified amount of characters. No need to 255 // continue this loop since nothing remains to be done. 256 257 if(spliceInsertOffset == undefined) 258 spliceInsertOffset = spliceIndex; 259 260 break; 261 } 262 263 spliceIndex -= segment.text.length; 264 265 segmentIndex += 1; 266 } 267 268 if(insert instanceof Buffer) 269 { 270 // If a buffer has been given, we insert copies of its segments at the 271 // specified position. 272 273 if(spliceInsertOffset == undefined) 274 spliceInsertOffset = this.segments.length; 275 276 for(var insertIndex = 0; insertIndex < insert.segments.length; 277 insertIndex ++) 278 { 279 this.segments.splice(spliceInsertOffset + insertIndex, 0, 280 insert.segments[insertIndex].copy()); 281 } 282 } 283 284 // Clean up since the splice operation might have fragmented some segments. 285 this.compact(); 286 } 287