1package eu.siacs.conversations.ui.util;
2
3import android.text.Spanned;
4import android.text.style.RelativeSizeSpan;
5
6import eu.siacs.conversations.Config;
7import eu.siacs.conversations.utils.UIHelper;
8
9public class QuoteHelper {
10
11
12 public static final char QUOTE_CHAR = '>';
13 public static final char QUOTE_END_CHAR = '<'; // used for one check, not for actual quoting
14 public static final char QUOTE_ALT_CHAR = '»';
15 public static final char QUOTE_ALT_END_CHAR = '«';
16
17 public static boolean isRelativeSizeSpanned(Spanned body, int pos) {
18 for (final var span : body.getSpans(pos, pos, RelativeSizeSpan.class)) {
19 if (body.getSpanEnd(span) != pos) return true;
20 }
21
22 return false;
23 }
24
25 public static boolean isPositionQuoteCharacter(CharSequence body, int pos) {
26 // second part of logical check actually goes against the logic indicated in the method name, since it also checks for context
27 // but it's very useful
28 return body.charAt(pos) == QUOTE_CHAR || isPositionAltQuoteStart(body, pos);
29 }
30
31 public static boolean isPositionQuoteEndCharacter(CharSequence body, int pos) {
32 return body.charAt(pos) == QUOTE_END_CHAR;
33 }
34
35 public static boolean isPositionAltQuoteCharacter(CharSequence body, int pos) {
36 return body.charAt(pos) == QUOTE_ALT_CHAR;
37 }
38
39 public static boolean isPositionAltQuoteEndCharacter(CharSequence body, int pos) {
40 return body.charAt(pos) == QUOTE_ALT_END_CHAR;
41 }
42
43 public static boolean isPositionAltQuoteStart(CharSequence body, int pos) {
44 return isPositionAltQuoteCharacter(body, pos)
45 && isPositionPrecededByPreQuote(body, pos)
46 && !isPositionFollowedByAltQuoteEnd(body, pos);
47 }
48
49 public static boolean isPositionFollowedByQuoteChar(CharSequence body, int pos) {
50 return body.length() > pos + 1 && isPositionQuoteCharacter(body, pos + 1);
51 }
52
53 /**
54 * 'Prequote' means anything we require or can accept in front of a QuoteChar.
55 */
56 public static boolean isPositionPrecededByPreQuote(CharSequence body, int pos) {
57 if (body instanceof Spanned) {
58 if (isRelativeSizeSpanned((Spanned) body, pos - 1)) return true;
59 }
60 return UIHelper.isPositionPrecededByLineStart(body, pos);
61 }
62
63 public static boolean isPositionQuoteStart(CharSequence body, int pos) {
64 return (isPositionQuoteCharacter(body, pos)
65 && isPositionPrecededByPreQuote(body, pos)
66 && (UIHelper.isPositionFollowedByQuoteableCharacter(body, pos)
67 || isPositionFollowedByQuoteChar(body, pos)));
68 }
69
70 public static boolean bodyContainsQuoteStart(CharSequence body) {
71 for (int i = 0; i < body.length(); i++) {
72 if (body instanceof Spanned) {
73 if (isRelativeSizeSpanned((Spanned) body, i)) continue;
74 }
75 if (isPositionQuoteStart(body, i)) {
76 return true;
77 }
78 }
79 return false;
80 }
81
82 public static boolean isPositionFollowedByAltQuoteEnd(CharSequence body, int pos) {
83 if (body.length() <= pos + 1 || Character.isWhitespace(body.charAt(pos + 1))) {
84 return false;
85 }
86 boolean previousWasWhitespace = false;
87 for (int i = pos + 1; i < body.length(); i++) {
88 char c = body.charAt(i);
89 if (c == '\n' || isPositionAltQuoteCharacter(body, i)) {
90 return false;
91 } else if (isPositionAltQuoteEndCharacter(body, i) && !previousWasWhitespace) {
92 return true;
93 } else {
94 previousWasWhitespace = Character.isWhitespace(c);
95 }
96 }
97 return false;
98 }
99
100 public static boolean isNestedTooDeeply(CharSequence line) {
101 if (isPositionQuoteStart(line, 0)) {
102 int nestingDepth = 1;
103 for (int i = 1; i < line.length(); i++) {
104 if (isPositionQuoteCharacter(line, i)) {
105 nestingDepth++;
106 } else if (line.charAt(i) != ' ') {
107 break;
108 }
109 }
110 return nestingDepth >= (Config.QUOTING_MAX_DEPTH);
111 }
112 return false;
113 }
114
115 public static String replaceAltQuoteCharsInText(String text) {
116 for (int i = 0; i < text.length(); i++) {
117 if (isPositionAltQuoteStart(text, i)) {
118 text = text.substring(0, i) + QUOTE_CHAR + text.substring(i + 1);
119 }
120 }
121 return text;
122 }
123
124 public static String quote(String text) {
125 text = replaceAltQuoteCharsInText(text);
126 return text
127 // first replace all '>' at the beginning of the line with nice and tidy '>>'
128 // for nested quoting
129 .replaceAll("(^|\n)(" + QUOTE_CHAR + ")", "$1$2$2")
130 // then find all other lines and have them start with a '> '
131 .replaceAll("(^|\n)(?!" + QUOTE_CHAR + ")(.*)", "$1> $2")
132 ;
133 }
134}