1 /* 2 * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>. 3 * Copyright 2021, Andrew Lindesay <apl@lindesay.co.nz>. 4 * All rights reserved. Distributed under the terms of the MIT License. 5 */ 6 7 #include "Paragraph.h" 8 9 #include <algorithm> 10 #include <stdio.h> 11 12 13 Paragraph::Paragraph() 14 : 15 fStyle() 16 { 17 } 18 19 20 Paragraph::Paragraph(const ParagraphStyle& style) 21 : 22 fStyle(style), 23 fTextSpans(), 24 fCachedLength(-1) 25 { 26 } 27 28 29 Paragraph::Paragraph(const Paragraph& other) 30 : 31 fStyle(other.fStyle), 32 fTextSpans(other.fTextSpans), 33 fCachedLength(other.fCachedLength) 34 { 35 } 36 37 38 Paragraph& 39 Paragraph::operator=(const Paragraph& other) 40 { 41 fStyle = other.fStyle; 42 fTextSpans = other.fTextSpans; 43 fCachedLength = other.fCachedLength; 44 45 return *this; 46 } 47 48 49 bool 50 Paragraph::operator==(const Paragraph& other) const 51 { 52 if (this == &other) 53 return true; 54 55 return fStyle == other.fStyle 56 && fTextSpans == other.fTextSpans; 57 } 58 59 60 bool 61 Paragraph::operator!=(const Paragraph& other) const 62 { 63 return !(*this == other); 64 } 65 66 67 int32 68 Paragraph::CountTextSpans() const 69 { 70 return static_cast<int32>(fTextSpans.size()); 71 } 72 73 74 const TextSpan& 75 Paragraph::TextSpanAtIndex(int32 index) const 76 { 77 return fTextSpans[index]; 78 } 79 80 81 void 82 Paragraph::SetStyle(const ParagraphStyle& style) 83 { 84 fStyle = style; 85 } 86 87 88 bool 89 Paragraph::Prepend(const TextSpan& span) 90 { 91 _InvalidateCachedLength(); 92 93 // Try to merge with first span if the TextStyles are equal 94 if (!fTextSpans.empty()) { 95 const TextSpan& firstSpan = fTextSpans[0]; 96 if (firstSpan.Style() == span.Style()) { 97 BString text(span.Text()); 98 text.Append(firstSpan.Text()); 99 fTextSpans[0] = TextSpan(text, span.Style()); 100 return true; 101 } 102 } 103 fTextSpans.push_back(span); 104 return true; 105 } 106 107 108 bool 109 Paragraph::Append(const TextSpan& span) 110 { 111 _InvalidateCachedLength(); 112 113 // Try to merge with last span if the TextStyles are equal 114 if (!fTextSpans.empty()) { 115 const TextSpan& lastSpan = fTextSpans[fTextSpans.size() - 1]; 116 if (lastSpan.Style() == span.Style()) { 117 BString text(lastSpan.Text()); 118 text.Append(span.Text()); 119 fTextSpans.pop_back(); 120 fTextSpans.push_back(TextSpan(text, span.Style())); 121 return true; 122 } 123 } 124 fTextSpans.push_back(span); 125 return true; 126 } 127 128 129 bool 130 Paragraph::Insert(int32 offset, const TextSpan& newSpan) 131 { 132 _InvalidateCachedLength(); 133 134 int32 index = 0; 135 136 { 137 int32 countTextSpans = static_cast<int32>(fTextSpans.size()); 138 while (index < countTextSpans) { 139 const TextSpan& span = fTextSpans[index]; 140 if (offset - span.CountChars() < 0) 141 break; 142 offset -= span.CountChars(); 143 index++; 144 } 145 146 if (countTextSpans == index) 147 return Append(newSpan); 148 } 149 150 // Try to merge with span at index if the TextStyles are equal 151 TextSpan span = fTextSpans[index]; 152 if (span.Style() == newSpan.Style()) { 153 span.Insert(offset, newSpan.Text()); 154 fTextSpans[index] = span; 155 return true; 156 } 157 158 if (offset == 0) { 159 if (index > 0) { 160 // Try to merge with TextSpan before if offset == 0 && index > 0 161 TextSpan span = fTextSpans[index - 1]; 162 if (span.Style() == newSpan.Style()) { 163 span.Insert(span.CountChars(), newSpan.Text()); 164 fTextSpans[index - 1] = span; 165 return true; 166 } 167 } 168 // Just insert the new span before the one at index 169 fTextSpans.insert(fTextSpans.begin() + index, newSpan); 170 return true; 171 } 172 173 // Split the span, 174 TextSpan spanBefore = span.SubSpan(0, offset); 175 TextSpan spanAfter = span.SubSpan(offset, span.CountChars() - offset); 176 177 fTextSpans[index] = spanBefore; 178 fTextSpans.insert(fTextSpans.begin() + (index + 1), newSpan); 179 fTextSpans.insert(fTextSpans.begin() + (index + 2), spanAfter); 180 return true; 181 } 182 183 184 bool 185 Paragraph::Remove(int32 offset, int32 length) 186 { 187 if (length == 0) 188 return true; 189 190 _InvalidateCachedLength(); 191 192 int32 index = 0; 193 194 { 195 int32 countTextSpans = static_cast<int32>(fTextSpans.size()); 196 while (index < countTextSpans) { 197 const TextSpan& span = fTextSpans[index]; 198 if (offset - span.CountChars() < 0) 199 break; 200 offset -= span.CountChars(); 201 index++; 202 } 203 204 if (index >= countTextSpans) 205 return false; 206 } 207 208 TextSpan span(fTextSpans[index]); 209 int32 removeLength = std::min(span.CountChars() - offset, length); 210 span.Remove(offset, removeLength); 211 length -= removeLength; 212 index += 1; 213 214 // Remove more spans if necessary 215 while (length > 0 && index < static_cast<int32>(fTextSpans.size())) { 216 int32 spanLength = fTextSpans[index].CountChars(); 217 if (spanLength <= length) { 218 fTextSpans.erase(fTextSpans.begin() + index); 219 length -= spanLength; 220 } else { 221 // Reached last span 222 removeLength = std::min(length, spanLength); 223 TextSpan lastSpan = fTextSpans[index].SubSpan( 224 removeLength, spanLength - removeLength); 225 // Try to merge with first span, otherwise replace span at index 226 if (lastSpan.Style() == span.Style()) { 227 span.Insert(span.CountChars(), lastSpan.Text()); 228 fTextSpans.erase(fTextSpans.begin() + index); 229 } else { 230 fTextSpans[index] = lastSpan; 231 } 232 233 break; 234 } 235 } 236 237 // See if anything from the TextSpan at offset remained, keep it as empty 238 // span if it is the last remaining span. 239 index--; 240 if (span.CountChars() > 0 || static_cast<int32>(fTextSpans.size()) == 1) { 241 fTextSpans[index] = span; 242 } else { 243 fTextSpans.erase(fTextSpans.begin() + index); 244 index--; 245 } 246 247 // See if spans can be merged after one has been removed. 248 if (index >= 0 && index + 1 < static_cast<int32>(fTextSpans.size())) { 249 const TextSpan& span1 = fTextSpans[index]; 250 const TextSpan& span2 = fTextSpans[index + 1]; 251 if (span1.Style() == span2.Style()) { 252 span = span1; 253 span.Append(span2.Text()); 254 fTextSpans[index] = span; 255 fTextSpans.erase(fTextSpans.begin() + (index + 1)); 256 } 257 } 258 259 return true; 260 } 261 262 263 void 264 Paragraph::Clear() 265 { 266 fTextSpans.clear(); 267 } 268 269 270 int32 271 Paragraph::Length() const 272 { 273 if (fCachedLength >= 0) 274 return fCachedLength; 275 276 int32 length = 0; 277 std::vector<TextSpan>::const_iterator it; 278 for (it = fTextSpans.begin(); it != fTextSpans.end(); it++) { 279 const TextSpan& span = *it; 280 length += span.CountChars(); 281 } 282 283 fCachedLength = length; 284 return length; 285 } 286 287 288 bool 289 Paragraph::IsEmpty() const 290 { 291 return fTextSpans.empty(); 292 } 293 294 295 bool 296 Paragraph::EndsWith(BString string) const 297 { 298 int length = Length(); 299 int endLength = string.CountChars(); 300 int start = length - endLength; 301 BString end = Text(start, endLength); 302 return end == string; 303 } 304 305 306 BString 307 Paragraph::Text() const 308 { 309 BString result; 310 std::vector<TextSpan>::const_iterator it; 311 for (it = fTextSpans.begin(); it != fTextSpans.end(); it++) { 312 const TextSpan& span = *it; 313 result << span.Text(); 314 } 315 return result; 316 } 317 318 319 BString 320 Paragraph::Text(int32 start, int32 length) const 321 { 322 Paragraph subParagraph = SubParagraph(start, length); 323 return subParagraph.Text(); 324 } 325 326 327 Paragraph 328 Paragraph::SubParagraph(int32 start, int32 length) const 329 { 330 if (start < 0) 331 start = 0; 332 333 if (start == 0 && length == Length()) 334 return *this; 335 336 Paragraph result(fStyle); 337 338 std::vector<TextSpan>::const_iterator it; 339 for (it = fTextSpans.begin(); it != fTextSpans.end(); it++) { 340 const TextSpan& span = *it; 341 int32 spanLength = span.CountChars(); 342 if (spanLength == 0) 343 continue; 344 if (start > spanLength) { 345 // Skip span if its before start 346 start -= spanLength; 347 continue; 348 } 349 350 // Remaining span length after start 351 spanLength -= start; 352 int32 copyLength = std::min(spanLength, length); 353 354 if (start == 0 && length == spanLength) 355 result.Append(span); 356 else 357 result.Append(span.SubSpan(start, copyLength)); 358 359 length -= copyLength; 360 if (length == 0) 361 break; 362 363 // Next span is copied from its beginning 364 start = 0; 365 } 366 367 return result; 368 } 369 370 371 void 372 Paragraph::PrintToStream() const 373 { 374 int32 spanCount = static_cast<int32>(fTextSpans.size()); 375 if (spanCount == 0) { 376 printf(" <p/>\n"); 377 return; 378 } 379 printf(" <p>\n"); 380 for (int32 i = 0; i < spanCount; i++) { 381 const TextSpan& span = fTextSpans[i]; 382 if (span.CountChars() == 0) 383 printf(" <span/>\n"); 384 else { 385 BString text = span.Text(); 386 text.ReplaceAll("\n", "\\n"); 387 printf(" <span>%s</span>\n", text.String()); 388 } 389 } 390 printf(" </p>\n"); 391 } 392 393 394 // #pragma mark - 395 396 397 void 398 Paragraph::_InvalidateCachedLength() 399 { 400 fCachedLength = -1; 401 } 402