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}