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 String removed = "";
82 int next;
83 outer:
84 for (int i = start; i < end; i = next) {
85 Element out = outer;
86 next = text.nextSpanTransition(i, end, CharacterStyle.class);
87 CharacterStyle[] style = text.getSpans(i, next, CharacterStyle.class);
88 for (int j = 0; j < style.length; j++) {
89 final var userFlags = ((text.getSpanFlags(style[j]) & Spanned.SPAN_USER) >> Spanned.SPAN_USER_SHIFT) & 0xf;
90 if (userFlags == StylingHelper.XHTML_REMOVE) {
91 removed = text.subSequence(i, next).toString();
92 continue outer;
93 }
94
95 if (style[j] instanceof StyleSpan) {
96 int s = ((StyleSpan) style[j]).getStyle();
97 if ((s & Typeface.BOLD) != 0) {
98 if (userFlags == StylingHelper.XHTML_EMPHASIS) {
99 out = out.addChild("strong");
100 } else {
101 out = out.addChild("b");
102 }
103 }
104 if ((s & Typeface.ITALIC) != 0) {
105 if (userFlags == StylingHelper.XHTML_EMPHASIS) {
106 out = out.addChild("em");
107 } else {
108 out = out.addChild("i");
109 }
110 }
111 }
112 if (style[j] instanceof TypefaceSpan) {
113 if (userFlags == StylingHelper.XHTML_CODE) {
114 out = out.addChild("code");
115 if (removed.length() > 3) out.setAttribute("class", "language-" + removed.substring(3).trim());
116 } else {
117 String s = ((TypefaceSpan) style[j]).getFamily();
118 if ("monospace".equals(s)) {
119 out = out.addChild("tt");
120 }
121 }
122 }
123 if (style[j] instanceof SuperscriptSpan) {
124 out = out.addChild("sup");
125 }
126 if (style[j] instanceof SubscriptSpan) {
127 out = out.addChild("sub");
128 }
129 if (style[j] instanceof UnderlineSpan) {
130 out = out.addChild("u");
131 }
132 if (style[j] instanceof StrikethroughSpan) {
133 out = out.addChild("span");
134 out.setAttribute("style", "text-decoration:line-through;");
135 }
136 if (style[j] instanceof URLSpan) {
137 out = out.addChild("a");
138 out.setAttribute("href", ((URLSpan) style[j]).getURL());
139 }
140 if (style[j] instanceof ImageSpan) {
141 String source = ((ImageSpan) style[j]).getSource();
142 if (source != null && source.length() > 0 && source.charAt(0) == 'z') {
143 try {
144 source = BobTransfer.uri(Cid.decode(source)).toString();
145 } catch (final Exception e) { }
146 }
147 out = out.addChild("img");
148 out.setAttribute("src", source);
149 out.setAttribute("alt", text.subSequence(i, next).toString());
150 continue outer;
151 }
152 if (style[j] instanceof AbsoluteSizeSpan) {
153 try {
154 AbsoluteSizeSpan s = ((AbsoluteSizeSpan) style[j]);
155 float sizeDip = s.getSize();
156 if (!s.getDip()) {
157 Class activityThreadClass = Class.forName("android.app.ActivityThread");
158 Application application = (Application) activityThreadClass.getMethod("currentApplication").invoke(null);
159 sizeDip /= application.getResources().getDisplayMetrics().density;
160 }
161 // px in CSS is the equivalance of dip in Android
162 out = out.addChild("span");
163 out.setAttribute("style", String.format("font-size:%.0fpx;", sizeDip));
164 } catch (final Exception e) { }
165 }
166 if (style[j] instanceof RelativeSizeSpan) {
167 float sizeEm = ((RelativeSizeSpan) style[j]).getSizeChange();
168 out = out.addChild("span");
169 out.setAttribute("style", String.format("font-size:%.0fem;", sizeEm));
170 }
171 if (style[j] instanceof ForegroundColorSpan) {
172 int color = ((ForegroundColorSpan) style[j]).getForegroundColor();
173 out = out.addChild("span");
174 out.setAttribute("style", String.format("color:#%06X;", 0xFFFFFF & color));
175 }
176 if (style[j] instanceof BackgroundColorSpan) {
177 int color = ((BackgroundColorSpan) style[j]).getBackgroundColor();
178 out = out.addChild("span");
179 out.setAttribute("style", String.format("background-color:#%06X;", 0xFFFFFF & color));
180 }
181 }
182 String content = text.subSequence(i, next).toString();
183 boolean prevSpace = false;
184 for (int c = 0; c < content.length(); c++) {
185 if (content.charAt(c) == '\n') {
186 prevSpace = false;
187 out.addChild("br");
188 } else if (prevSpace && content.charAt(c) == ' ') {
189 prevSpace = false;
190 out.addChild(new TextNode("\u00A0"));
191 } else {
192 prevSpace = content.charAt(c) == ' ';
193 out.addChild(new TextNode("" + content.charAt(c)));
194 }
195 }
196 }
197 }
198}