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