SpannedToXHTML.java

  1package com.cheogram.android;
  2
  3import android.app.Application;
  4import android.graphics.Typeface;
  5import android.text.Spanned;
  6import android.text.SpannableStringBuilder;
  7import android.text.ParcelableSpan;
  8import android.text.style.AbsoluteSizeSpan;
  9import android.text.style.AlignmentSpan;
 10import android.text.style.BackgroundColorSpan;
 11import android.text.style.BulletSpan;
 12import android.text.style.CharacterStyle;
 13import android.text.style.ForegroundColorSpan;
 14import android.text.style.ImageSpan;
 15import android.text.style.ParagraphStyle;
 16import android.text.style.RelativeSizeSpan;
 17import android.text.style.StrikethroughSpan;
 18import android.text.style.StyleSpan;
 19import android.text.style.SubscriptSpan;
 20import android.text.style.SuggestionSpan;
 21import android.text.style.SuperscriptSpan;
 22import android.text.style.TypefaceSpan;
 23import android.text.style.URLSpan;
 24import android.text.style.UnderlineSpan;
 25import android.view.inputmethod.BaseInputConnection;
 26
 27import io.ipfs.cid.Cid;
 28
 29import java.util.ArrayList;
 30
 31import eu.siacs.conversations.ui.text.QuoteSpan;
 32import eu.siacs.conversations.utils.StylingHelper;
 33import eu.siacs.conversations.xml.Element;
 34import eu.siacs.conversations.xml.TextNode;
 35
 36public class SpannedToXHTML {
 37	private static SpannableStringBuilder cleanSpans(Spanned text) {
 38		SpannableStringBuilder newText = new SpannableStringBuilder(text);
 39		ParcelableSpan[] spans = newText.getSpans(0, newText.length(), ParcelableSpan.class);
 40		for (final var span : spans) {
 41			final var userFlags = (text.getSpanFlags(span) & Spanned.SPAN_USER) >> Spanned.SPAN_USER_SHIFT;
 42			if (span instanceof SuggestionSpan || userFlags == StylingHelper.XHTML_IGNORE) newText.removeSpan(span);
 43		}
 44		BaseInputConnection.removeComposingSpans(newText);
 45		return newText;
 46	}
 47
 48	public static boolean isPlainText(Spanned text) {
 49		SpannableStringBuilder cleanText = cleanSpans(text);
 50		CharacterStyle[] style = cleanText.getSpans(0, cleanText.length(), CharacterStyle.class);
 51		return style.length < 1;
 52	}
 53
 54	public static Element append(Element out, Spanned text) {
 55		SpannableStringBuilder newText = cleanSpans(text);
 56		doBlock(out, new ArrayList<>(), newText, 0, newText.length());
 57		return out;
 58	}
 59
 60	private static void doBlock(Element out, ArrayList<Object> parents, Spanned text, int start, int end) {
 61		int next;
 62		outer:
 63		for (int i = start; i < end; i = next) {
 64			next = text.nextSpanTransition(i, end, QuoteSpan.class);
 65			QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class);
 66			for (final var quote : quotes) {
 67				if (!parents.contains(quote)) {
 68					final var us = new ArrayList<>(parents);
 69					us.add(quote);
 70					next = text.getSpanEnd(quote);
 71					doBlock(out.addChild("blockquote"), us, text, i, next);
 72					if (next < text.length() && text.charAt(next) == '\n') next++;
 73					continue outer;
 74				}
 75			}
 76			withinParagraph(out, text, i, (next == end && text.charAt(end-1) == '\n') ? next - 1 : next);
 77		}
 78	}
 79
 80	private static void withinParagraph(Element outer, Spanned text, int start, int end) {
 81		int next;
 82		outer:
 83		for (int i = start; i < end; i = next) {
 84			Element out = outer;
 85			next = text.nextSpanTransition(i, end, CharacterStyle.class);
 86			CharacterStyle[] style = text.getSpans(i, next, CharacterStyle.class);
 87			for (int j = 0; j < style.length; j++) {
 88				final var userFlags = (text.getSpanFlags(style[j]) & Spanned.SPAN_USER) >> Spanned.SPAN_USER_SHIFT;
 89				if (userFlags == StylingHelper.XHTML_REMOVE) {
 90					continue outer;
 91				}
 92
 93				if (style[j] instanceof StyleSpan) {
 94					int s = ((StyleSpan) style[j]).getStyle();
 95					if ((s & Typeface.BOLD) != 0) {
 96						if (userFlags == StylingHelper.XHTML_EMPHASIS) {
 97							out = out.addChild("strong");
 98						} else {
 99							out = out.addChild("b");
100						}
101					}
102					if ((s & Typeface.ITALIC) != 0) {
103						if (userFlags == StylingHelper.XHTML_EMPHASIS) {
104							out = out.addChild("em");
105						} else {
106							out = out.addChild("i");
107						}
108					}
109				}
110				if (style[j] instanceof TypefaceSpan) {
111					String s = ((TypefaceSpan) style[j]).getFamily();
112					if ("monospace".equals(s)) {
113						out = out.addChild("tt");
114					}
115				}
116				if (style[j] instanceof SuperscriptSpan) {
117					out = out.addChild("sup");
118				}
119				if (style[j] instanceof SubscriptSpan) {
120					out = out.addChild("sub");
121				}
122				if (style[j] instanceof UnderlineSpan) {
123					out = out.addChild("u");
124				}
125				if (style[j] instanceof StrikethroughSpan) {
126					out = out.addChild("span");
127					out.setAttribute("style", "text-decoration:line-through;");
128				}
129				if (style[j] instanceof URLSpan) {
130					out = out.addChild("a");
131					out.setAttribute("href", ((URLSpan) style[j]).getURL());
132				}
133				if (style[j] instanceof ImageSpan) {
134					String source = ((ImageSpan) style[j]).getSource();
135					if (source != null && source.length() > 0 && source.charAt(0) == 'z') {
136						try {
137							source = BobTransfer.uri(Cid.decode(source)).toString();
138						} catch (final Exception e) { }
139					}
140					out = out.addChild("img");
141					out.setAttribute("src", source);
142					out.setAttribute("alt", text.subSequence(i, next).toString());
143					continue outer;
144				}
145				if (style[j] instanceof AbsoluteSizeSpan) {
146					try {
147						AbsoluteSizeSpan s = ((AbsoluteSizeSpan) style[j]);
148						float sizeDip = s.getSize();
149						if (!s.getDip()) {
150							Class activityThreadClass = Class.forName("android.app.ActivityThread");
151							Application application = (Application) activityThreadClass.getMethod("currentApplication").invoke(null);
152							sizeDip /= application.getResources().getDisplayMetrics().density;
153						}
154						// px in CSS is the equivalance of dip in Android
155						out = out.addChild("span");
156						out.setAttribute("style", String.format("font-size:%.0fpx;", sizeDip));
157					} catch (final Exception e) { }
158				}
159				if (style[j] instanceof RelativeSizeSpan) {
160					float sizeEm = ((RelativeSizeSpan) style[j]).getSizeChange();
161					out = out.addChild("span");
162					out.setAttribute("style", String.format("font-size:%.0fem;", sizeEm));
163				}
164				if (style[j] instanceof ForegroundColorSpan) {
165					int color = ((ForegroundColorSpan) style[j]).getForegroundColor();
166					out = out.addChild("span");
167					out.setAttribute("style", String.format("color:#%06X;", 0xFFFFFF & color));
168				}
169				if (style[j] instanceof BackgroundColorSpan) {
170					int color = ((BackgroundColorSpan) style[j]).getBackgroundColor();
171					out = out.addChild("span");
172					out.setAttribute("style", String.format("background-color:#%06X;", 0xFFFFFF & color));
173				}
174			}
175			String content = text.subSequence(i, next).toString();
176			boolean prevSpace = false;
177			for (int c = 0; c < content.length(); c++) {
178				if (content.charAt(c) == '\n') {
179					prevSpace = false;
180					out.addChild("br");
181				} else if (prevSpace && content.charAt(c) == ' ') {
182					prevSpace = false;
183					out.addChild(new TextNode("\u00A0"));
184				} else {
185					prevSpace = content.charAt(c) == ' ';
186					out.addChild(new TextNode("" + content.charAt(c)));
187				}
188			}
189		}
190	}
191}