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
13Paragraph::Paragraph()
14	:
15	fStyle()
16{
17}
18
19
20Paragraph::Paragraph(const ParagraphStyle& style)
21	:
22	fStyle(style),
23	fTextSpans(),
24	fCachedLength(-1)
25{
26}
27
28
29Paragraph::Paragraph(const Paragraph& other)
30	:
31	fStyle(other.fStyle),
32	fTextSpans(other.fTextSpans),
33	fCachedLength(other.fCachedLength)
34{
35}
36
37
38Paragraph&
39Paragraph::operator=(const Paragraph& other)
40{
41	fStyle = other.fStyle;
42	fTextSpans = other.fTextSpans;
43	fCachedLength = other.fCachedLength;
44
45	return *this;
46}
47
48
49bool
50Paragraph::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
60bool
61Paragraph::operator!=(const Paragraph& other) const
62{
63	return !(*this == other);
64}
65
66
67int32
68Paragraph::CountTextSpans() const
69{
70	return static_cast<int32>(fTextSpans.size());
71}
72
73
74const TextSpan&
75Paragraph::TextSpanAtIndex(int32 index) const
76{
77	return fTextSpans[index];
78}
79
80
81void
82Paragraph::SetStyle(const ParagraphStyle& style)
83{
84	fStyle = style;
85}
86
87
88bool
89Paragraph::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
108bool
109Paragraph::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
129bool
130Paragraph::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
184bool
185Paragraph::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
263void
264Paragraph::Clear()
265{
266	fTextSpans.clear();
267}
268
269
270int32
271Paragraph::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
288bool
289Paragraph::IsEmpty() const
290{
291	return fTextSpans.empty();
292}
293
294
295bool
296Paragraph::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
306BString
307Paragraph::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
319BString
320Paragraph::Text(int32 start, int32 length) const
321{
322	Paragraph subParagraph = SubParagraph(start, length);
323	return subParagraph.Text();
324}
325
326
327Paragraph
328Paragraph::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
371void
372Paragraph::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
397void
398Paragraph::_InvalidateCachedLength()
399{
400	fCachedLength = -1;
401}
402