styling: introduce support for code blocks

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/utils/ImStyleParser.java | 46 ++++
src/main/java/eu/siacs/conversations/utils/StylingHelper.java | 18 +
2 files changed, 47 insertions(+), 17 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/utils/ImStyleParser.java 🔗

@@ -35,19 +35,29 @@ import java.util.List;
 
 public class ImStyleParser {
 
-	final static List<Character> KEYWORDS = Arrays.asList('*', '_', '~', '`');
-	final static List<Character> NO_SUB_PARSING_KEYWORDS = Arrays.asList('`');
-	final static boolean ALLOW_EMPTY = false;
+	private final static List<Character> KEYWORDS = Arrays.asList('*', '_', '~', '`');
+	private final static List<Character> NO_SUB_PARSING_KEYWORDS = Arrays.asList('`');
+	private final static List<Character> BLOCK_KEYWORDS = Arrays.asList('`');
+	private final static boolean ALLOW_EMPTY = false;
 
 	public static List<Style> parse(CharSequence text) {
 		return parse(text, 0, text.length() - 1);
 	}
 
-	public static List<Style> parse(CharSequence text, int start, int end) {
+	private static List<Style> parse(CharSequence text, int start, int end) {
 		List<Style> styles = new ArrayList<>();
 		for (int i = start; i <= end; ++i) {
 			char c = text.charAt(i);
 			if (KEYWORDS.contains(c) && precededByWhiteSpace(text, i, start) && !followedByWhitespace(text, i, end)) {
+				if (BLOCK_KEYWORDS.contains(c) && isCharRepeatedTwoTimes(text, c, i + 1, end)) {
+					int to = seekEndBlock(text, c, i + 3, end);
+					if (to != -1 && (to != i + 5 || ALLOW_EMPTY)) {
+						String keyword = String.valueOf(c) + String.valueOf(c) + String.valueOf(c);
+						styles.add(new Style(keyword, i, to));
+						i = to;
+						continue;
+					}
+				}
 				int to = seekEnd(text, c, i + 1, end);
 				if (to != -1 && (to != i + 1 || ALLOW_EMPTY)) {
 					styles.add(new Style(c, i, to));
@@ -61,6 +71,10 @@ public class ImStyleParser {
 		return styles;
 	}
 
+	private static boolean isCharRepeatedTwoTimes(CharSequence text, char c, int index, int end) {
+		return index + 1 <= end && text.charAt(index) == c && text.charAt(index) == c;
+	}
+
 	private static boolean precededByWhiteSpace(CharSequence text, int index, int start) {
 		return index == start || Character.isWhitespace(text.charAt(index - 1));
 	}
@@ -81,20 +95,34 @@ public class ImStyleParser {
 		return -1;
 	}
 
+	private static int seekEndBlock(CharSequence text, char needle, int start, int end) {
+		for (int i = start; i <= end; ++i) {
+			char c = text.charAt(i);
+			if (c == needle && isCharRepeatedTwoTimes(text, needle, i + 1, end)) {
+				return i + 2;
+			}
+		}
+		return -1;
+	}
+
 	public static class Style {
 
-		private final char c;
+		private final String keyword;
 		private final int start;
 		private final int end;
 
-		public Style(char c, int start, int end) {
-			this.c = c;
+		public Style(char character, int start, int end) {
+			this(String.valueOf(character), start, end);
+		}
+
+		public Style(String keyword, int start, int end) {
+			this.keyword = keyword;
 			this.start = start;
 			this.end = end;
 		}
 
-		public char getCharacter() {
-			return c;
+		public String getKeyword() {
+			return keyword;
 		}
 
 		public int getStart() {

src/main/java/eu/siacs/conversations/utils/StylingHelper.java 🔗

@@ -67,21 +67,23 @@ public class StylingHelper {
 
 	public static void format(final Editable editable, @ColorInt int textColor) {
 		for (ImStyleParser.Style style : ImStyleParser.parse(editable)) {
-			editable.setSpan(createSpanForStyle(style), style.getStart() + 1, style.getEnd(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-			makeKeywordOpaque(editable, style.getStart(), style.getStart() + 1, textColor);
-			makeKeywordOpaque(editable, style.getEnd(), style.getEnd() + 1, textColor);
+			final int keywordLength = style.getKeyword().length();
+			editable.setSpan(createSpanForStyle(style), style.getStart() + keywordLength, style.getEnd() - keywordLength + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+			makeKeywordOpaque(editable, style.getStart(), style.getStart() + keywordLength, textColor);
+			makeKeywordOpaque(editable, style.getEnd() - keywordLength + 1, style.getEnd() + 1, textColor);
 		}
 	}
 
 	private static ParcelableSpan createSpanForStyle(ImStyleParser.Style style) {
-		switch (style.getCharacter()) {
-			case '*':
+		switch (style.getKeyword()) {
+			case "*":
 				return new StyleSpan(Typeface.BOLD);
-			case '_':
+			case "_":
 				return new StyleSpan(Typeface.ITALIC);
-			case '~':
+			case "~":
 				return new StrikethroughSpan();
-			case '`':
+			case "`":
+			case "```":
 				return new TypefaceSpan("monospace");
 			default:
 				throw new AssertionError("Unknown Style");