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}