//
//
"); line = line.substring(1); // > if (line.startsWith(" >")) line = line.substring(1); } if (tlevel > 0) if (line.length() > 0 && line.charAt(0) == ' ') line = line.substring(1); // Closing quotes for (int i = 0; i < level - tlevel; i++) sb.append(""); level = tlevel; } // Tabs characters StringBuilder sbl = new StringBuilder(); for (int j = 0; j < line.length(); j++) { char kar = line.charAt(j); if (kar == '\t') { sbl.append(' '); while (sbl.length() % TAB_SIZE != 0) sbl.append(' '); } else sbl.append(kar); } line = sbl.toString(); // Html characters // This will handle spaces / word wrapping as well line = Html.escapeHtml(line); sb.append(line); if (view || l + 1 < lines.length || text.endsWith("\n")) sb.append("
String width = img.attr("width").replace("px", "").trim();
String height = img.attr("height").replace("px", "").trim();
if (TextUtils.isEmpty(width) || TextUtils.isEmpty(height))
return false;
try {
int w = Integer.parseInt(width);
int h = Integer.parseInt(height);
if (w == 0 && h != 0)
w = h;
if (w != 0 && h == 0)
h = w;
return (w * h <= TRACKING_PIXEL_SURFACE);
} catch (NumberFormatException ignored) {
return false;
}
}
private static boolean isTrackingHost(Context context, String host, boolean disconnect_images) {
if (TRACKING_HOSTS.contains(host))
return true;
if (disconnect_images && DisconnectBlacklist.isTrackingImage(context, host))
return true;
return false;
}
static void embedInlineImages(Context context, long id, Document document, boolean local) throws IOException {
DB db = DB.getInstance(context);
for (Element img : document.select("img")) {
String src = img.attr("src");
if (src.startsWith("cid:")) {
String cid = '<' + src.substring(4) + '>';
EntityAttachment attachment = db.attachment().getAttachment(id, cid);
if (attachment != null && attachment.available) {
File file = attachment.getFile(context);
if (local) {
Uri uri = FileProviderEx.getUri(context, BuildConfig.APPLICATION_ID, file, attachment.name);
img.attr("src", uri.toString());
Log.i("Inline image uri=" + uri);
} else {
img.attr("src", ImageHelper.getDataUri(file, attachment.type));
Log.i("Inline image type=" + attachment.type);
}
}
}
}
}
static void setViewport(Document document, boolean overview) {
// https://developer.mozilla.org/en-US/docs/Mozilla/Mobile/Viewport_meta_tag
// https://drafts.csswg.org/css-device-adapt/#viewport-meta
Elements meta = document.select("meta").select("[name=viewport]");
// Note that the browser will recognize meta elements in the body too
if (overview) {
// fit width
meta.remove();
document.head().prependElement("meta")
.attr("name", "viewport")
.attr("content", "width=device-width");
} else {
if (meta.size() == 1) {
String content = meta.attr("content");
String[] param = content.split("[;,]");
for (int i = 0; i < param.length; i++) {
String[] kv = param[i].split("=");
if (kv.length == 2) {
String key = kv[0]
.replaceAll("\\s+", "")
.toLowerCase(Locale.ROOT);
switch (key) {
case "user-scalable":
kv[1] = "yes";
param[i] = TextUtils.join("=", kv);
break;
case "minimum-scale":
case "maximum-scale":
kv[0] = "disabled-scaling";
param[i] = TextUtils.join("=", kv);
break;
}
}
}
meta.attr("content", TextUtils.join(",", param));
} else {
meta.remove();
document.head().prependElement("meta")
.attr("name", "viewport")
.attr("content", "width=device-width, initial-scale=1.0");
}
}
if (BuildConfig.DEBUG)
Log.i(document.head().html());
}
static void overrideWidth(Document document) {
List
//d.body().select("div.moz-signature").remove();
//d.body().select("pre.moz-signature").remove();
// Apple:
for (Element br : d.body().select("br#lineBreakAtBeginningOfSignature")) {
Element next = br.nextElementSibling();
if (next != null && "div".equals(next.tagName())) {
br.remove();
next.remove();
}
}
// Usenet style signature
d.body().filter(new NodeFilter() {
private boolean remove = false;
private boolean noremove = false;
@Override
public FilterResult head(Node node, int depth) {
if (node instanceof TextNode) {
TextNode tnode = (TextNode) node;
String text = tnode.getWholeText()
.replaceAll("[\r\n]+$", "")
.replaceAll("^[\r\n]+", "");
if ("-- ".equals(text)) {
if (tnode.getWholeText().endsWith("\n"))
remove = true;
else {
Node next = node.nextSibling();
if (next == null) {
Node parent = node.parent();
if (parent != null)
next = parent.nextSibling();
}
if (next != null && "br".equals(next.nodeName()))
remove = true;
}
}
} else if (node instanceof Element) {
Element element = (Element) node;
if (remove && "blockquote".equals(element.tagName()))
noremove = true;
}
return (remove && !noremove
? FilterResult.REMOVE : FilterResult.CONTINUE);
}
@Override
public FilterResult tail(Node node, int depth) {
return FilterResult.CONTINUE;
}
});
}
static boolean removeQuotes(Document d, boolean all) {
Elements quotes = d.body().select(".fairemail_quote");
if (!quotes.isEmpty()) {
quotes.remove();
return true;
}
// Gmail
quotes = d.body().select(".gmail_quote");
if (!quotes.isEmpty()) {
quotes.remove();
return true;
}
// Outlook:
quotes = d.body().select("div#appendonsend");
if (!quotes.isEmpty()) {
quotes.nextAll().remove();
quotes.remove();
return true;
}
// ms-outlook-mobile
quotes = d.body().select("div#divRplyFwdMsg");
if (!quotes.isEmpty()) {
quotes.nextAll().remove();
quotes.remove();
return true;
}
// Microsoft Word 15
quotes = d.body().select("div#mail-editor-reference-message-container");
if (!quotes.isEmpty()) {
quotes.remove();
return true;
}
// Web.de: 0)
preview = preview.substring(0, space + 1);
return preview + "…";
}
@NonNull
static String getText(Context context, String html) {
Document d = sanitizeCompose(context, html, false);
truncate(d, getMaxFormatTextSize(context));
SpannableStringBuilder ssb = fromDocument(context, d, null, null);
for (UnderlineSpan span : ssb.getSpans(0, ssb.length(), UnderlineSpan.class)) {
int start = ssb.getSpanStart(span);
int end = ssb.getSpanEnd(span);
ssb.insert(end, "_");
ssb.insert(start, "_");
}
for (StyleSpan span : ssb.getSpans(0, ssb.length(), StyleSpan.class)) {
int start = ssb.getSpanStart(span);
int end = ssb.getSpanEnd(span);
if (span.getStyle() == Typeface.ITALIC) {
ssb.insert(end, "/");
ssb.insert(start, "/");
} else if (span.getStyle() == Typeface.BOLD) {
ssb.insert(end, "*");
ssb.insert(start, "*");
}
}
for (URLSpan span : ssb.getSpans(0, ssb.length(), URLSpan.class)) {
String url = span.getURL();
if (TextUtils.isEmpty(url))
continue;
if (url.toLowerCase(Locale.ROOT).startsWith("mailto:"))
url = url.substring("mailto:".length());
int start = ssb.getSpanStart(span);
int end = ssb.getSpanEnd(span);
String text = ssb.subSequence(start, end).toString();
if (!text.contains(url))
ssb.insert(end, "[" + url + "]");
}
for (ImageSpan span : ssb.getSpans(0, ssb.length(), ImageSpan.class)) {
String source = span.getSource();
if (TextUtils.isEmpty(source))
continue;
int start = ssb.getSpanStart(span);
int end = ssb.getSpanEnd(span);
if (!source.toLowerCase(Locale.ROOT).startsWith("data:"))
ssb.insert(end, "[" + context.getString(R.string.title_avatar) + "]");
for (int i = start; i < end; i++)
if (ssb.charAt(i) == '\uFFFC') {
ssb.delete(i, i + 1);
end--;
}
}
for (BulletSpan span : ssb.getSpans(0, ssb.length(), BulletSpan.class)) {
int start = ssb.getSpanStart(span);
if (span instanceof NumberSpan) {
NumberSpan ns = (NumberSpan) span;
ssb.insert(start, ns.getIndex() + ". ");
int level = ns.getLevel();
for (int l = 1; l <= level; l++)
ssb.insert(start, " ");
} else {
if (span instanceof BulletSpanEx) {
BulletSpanEx bs = (BulletSpanEx) span;
if (!"none".equals(bs.getLType()))
ssb.insert(start, "* ");
int level = bs.getLevel();
for (int l = 1; l <= level; l++)
ssb.insert(start, " ");
} else
ssb.insert(start, "* ");
}
}
for (LineSpan span : ssb.getSpans(0, ssb.length(), LineSpan.class)) {
int start = ssb.getSpanStart(span);
int end = ssb.getSpanEnd(span);
ssb.replace(start, end, LINE);
}
// https://tools.ietf.org/html/rfc3676#section-4.5
for (QuoteSpan span : ssb.getSpans(0, ssb.length(), QuoteSpan.class)) {
int start = ssb.getSpanStart(span);
int end = ssb.getSpanEnd(span);
for (int i = end - 2; i >= start; i--)
if (ssb.charAt(i) == '\n')
if (i + 1 < ssb.length() && ssb.charAt(i + 1) == '>')
ssb.insert(i + 1, ">");
else
ssb.insert(i + 1, "> ");
if (start < ssb.length())
ssb.insert(start, ssb.charAt(start) == '>' ? ">" : "> ");
}
return ssb.toString();
}
static SpannableStringBuilder highlightHeaders(
Context context, Address[] from, Address[] to, Long time, String headers, boolean blocklist, boolean withReceived) {
SpannableStringBuilder ssb = new SpannableStringBuilderEx(headers.replaceAll("\\t", " "));
int textColorLink = Helper.resolveColor(context, android.R.attr.textColorLink);
int colorVerified = Helper.resolveColor(context, R.attr.colorVerified);
int colorWarning = Helper.resolveColor(context, R.attr.colorWarning);
int colorSeparator = Helper.resolveColor(context, R.attr.colorSeparator);
float stroke = context.getResources().getDisplayMetrics().density;
int index = 0;
for (String line : headers.split("\n")) {
if (line.length() > 0 && !Character.isWhitespace(line.charAt(0))) {
int colon = line.indexOf(':');
if (colon > 0)
ssb.setSpan(new ForegroundColorSpan(textColorLink), index, index + colon, 0);
}
index += line.length() + 1;
}
if (withReceived) {
ssb.append("\n\uFFFC"); // Object replacement character
ssb.setSpan(new LineSpan(colorSeparator, stroke, 0), ssb.length() - 1, ssb.length(), 0);
ssb.append('\n');
try {
// https://datatracker.ietf.org/doc/html/rfc2821#section-4.4
final DateFormat DTF = Helper.getDateTimeInstance(context, DateFormat.SHORT, DateFormat.MEDIUM);
MailDateFormat mdf = new MailDateFormat();
ByteArrayInputStream bis = new ByteArrayInputStream(headers.getBytes());
InternetHeaders iheaders = new InternetHeaders(bis, true);
Date tx = null;
String dh = iheaders.getHeader("Date", null);
try {
if (dh != null)
tx = mdf.parse(dh);
} catch (ParseException ex) {
Log.w(ex);
}
if (tx != null) {
ssb.append('\n');
int s = ssb.length();
ssb.append(DTF.format(tx));
ssb.setSpan(new StyleSpan(Typeface.BOLD), s, ssb.length(), 0);
}
if (from != null) {
ssb.append('\n');
int s = ssb.length();
ssb.append("from");
ssb.setSpan(new ForegroundColorSpan(textColorLink), s, ssb.length(), 0);
ssb.setSpan(new StyleSpan(Typeface.BOLD), s, ssb.length(), 0);
ssb.append(' ').append(MessageHelper.formatAddresses(from, true, false));
}
if (tx != null || from != null)
ssb.append('\n');
Date rx = null;
String[] received = iheaders.getHeader("Received");
if (received != null && received.length > 0) {
for (int i = received.length - 1; i >= 0; i--) {
ssb.append('\n');
String h = MimeUtility.unfold(received[i]);
int semi = h.lastIndexOf(';');
if (semi > 0) {
rx = mdf.parse(h, new ParsePosition(semi + 1));
h = h.substring(0, semi);
}
int s = ssb.length();
ssb.append('#').append(Integer.toString(received.length - i));
if (rx != null) {
ssb.append(' ').append(DTF.format(rx));
if (tx != null)
ssb.append(" \u0394")
.append(Helper.formatDuration(rx.getTime() - tx.getTime()));
}
ssb.setSpan(new StyleSpan(Typeface.BOLD), s, ssb.length(), 0);
if (blocklist && i == received.length - 1) {
Drawable d = ContextCompat.getDrawable(context, R.drawable.twotone_flag_24);
int iconSize = context.getResources().getDimensionPixelSize(R.dimen.menu_item_icon_size);
d.setBounds(0, 0, iconSize, iconSize);
d.setTint(colorWarning);
ssb.append(" \uFFFC"); // Object replacement character
ssb.setSpan(new ImageSpan(d), ssb.length() - 1, ssb.length(), 0);
if (!TextUtils.isEmpty(BuildConfig.MXTOOLBOX_URI)) {
final String header = received[i];
ClickableSpan click = new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
DnsBlockList.show(widget.getContext(), header);
}
};
ssb.setSpan(click, ssb.length() - 1, ssb.length(), 0);
}
}
ssb.append('\n');
int j = 0;
boolean p = false;
String[] w = h.split("\\s+");
while (j < w.length) {
if (w[j].startsWith("("))
p = true;
if (j > 0)
ssb.append(' ');
s = ssb.length();
ssb.append(w[j]);
if (!p && MessageHelper.RECEIVED_WORDS.contains(w[j].toLowerCase(Locale.ROOT))) {
ssb.setSpan(new ForegroundColorSpan(textColorLink), s, ssb.length(), 0);
ssb.setSpan(new StyleSpan(Typeface.BOLD), s, ssb.length(), 0);
}
if (w[j].endsWith(")"))
p = false;
j++;
}
Boolean tls = MessageHelper.isTLS(h, i == received.length - 1);
ssb.append(" TLS=");
int t = ssb.length();
ssb.append(tls == null ? "?" : Boolean.toString(tls));
if (tls != null)
ssb.setSpan(new ForegroundColorSpan(tls ? colorVerified : colorWarning), t, ssb.length(), 0);
ssb.append("\n");
}
}
if (time != null) {
ssb.append('\n');
int s = ssb.length();
ssb.append(DTF.format(time));
if (rx != null)
ssb.append(" \u0394")
.append(Helper.formatDuration(time - rx.getTime()));
ssb.setSpan(new StyleSpan(Typeface.BOLD), s, ssb.length(), 0);
}
if (to != null) {
ssb.append('\n');
int s = ssb.length();
ssb.append("to");
ssb.setSpan(new ForegroundColorSpan(textColorLink), s, ssb.length(), 0);
ssb.setSpan(new StyleSpan(Typeface.BOLD), s, ssb.length(), 0);
ssb.append(' ').append(MessageHelper.formatAddresses(to, true, false));
}
if (time != null || to != null)
ssb.append('\n');
} catch (Throwable ex) {
Log.w(ex);
}
}
return ssb;
}
static void highlightSearched(Context context, Document document, String query, boolean partial) {
try {
int color = Helper.resolveColor(context, R.attr.colorHighlight);
StringBuilder sb = new StringBuilder();
for (String word : query.trim().split("\\s+")) {
if (word.startsWith("+") || word.startsWith("-"))
continue;
for (String w : Fts4DbHelper.breakText(word).split("\\s+")) {
if (sb.length() > 0)
sb.append("\\s*");
sb.append(Pattern.quote(w));
}
}
if (partial) {
sb.insert(0, ".*?(");
sb.append(").*?");
} else {
sb.insert(0, ".*?\\b(");
sb.append(")\\b.*?");
}
// TODO: match für for fur
Pattern p = Pattern.compile(sb.toString(), Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
NodeTraversor.traverse(new NodeVisitor() {
@Override
public void head(Node node, int depth) {
if (node instanceof TextNode)
try {
TextNode tnode = (TextNode) node;
String text = tnode.getWholeText();
Matcher result = p.matcher(text);
int prev = 0;
Element holder = document.createElement("span");
while (result.find()) {
int start = result.start(1);
int end = result.end(1);
holder.appendText(text.substring(prev, start));
Element span = document.createElement("span");
span.attr("style", mergeStyles(
span.attr("style"),
"font-size:larger !important;" +
"font-weight:bold !important;" +
"background-color:" + encodeWebColor(color) + " !important"));
span.text(text.substring(start, end));
holder.appendChild(span);
prev = end;
}
if (prev == 0) // No matches
return;
if (prev < text.length())
holder.appendText(text.substring(prev));
tnode.before(holder);
tnode.text("");
} catch (Throwable ex) {
Log.e(ex);
}
}
@Override
public void tail(Node node, int depth) {
}
}, document);
} catch (Throwable ex) {
Log.e(ex);
}
}
static Document markText(Document document) {
for (Element mark : document.select("mark")) {
String style = mark.attr("style");
mark.attr("style", mergeStyles(style, "font-style: italic;"));
}
return document;
}
static void cleanup(Document d) {
// https://www.chromestatus.com/feature/5756335865987072
// Some messages contain 100 thousands of Apple spaces
if (false)
for (Element aspace : d.select(".Apple-converted-space")) {
Node next = aspace.nextSibling();
if (next instanceof TextNode) {
TextNode tnode = (TextNode) next;
tnode.text(" " + tnode.text());
aspace.remove();
} else
aspace.replaceWith(new TextNode(" "));
Log.i("Replaced Apple-converted-space");
}
}
static void quoteLimit(Document d, int maxLevel) {
for (Element bq : d.select("blockquote")) {
int level = 1;
Element parent = bq.parent();
while (parent != null) {
if ("blockquote".equals(parent.tagName())) // TODO: indentation
level++;
parent = parent.parent();
}
if (level >= maxLevel)
bq.html("…");
}
}
static boolean truncate(Document d, int max) {
final int[] length = new int[1];
NodeTraversor.filter(new NodeFilter() {
@Override
public FilterResult head(Node node, int depth) {
if (length[0] >= max)
return FilterResult.REMOVE;
else if (node instanceof TextNode) {
TextNode tnode = ((TextNode) node);
String text = tnode.getWholeText();
if (length[0] + text.length() >= max) {
text = text.substring(0, max - length[0]) + " ...";
tnode.text(text);
length[0] += text.length();
return FilterResult.SKIP_ENTIRELY;
} else
length[0] += text.length();
}
return FilterResult.CONTINUE;
}
@Override
public FilterResult tail(Node node, int depth) {
return FilterResult.CONTINUE;
}
}, d.body());
Log.i("Message size=" + length[0]);
return (length[0] > max);
}
static boolean contains(Document d, String[] texts) {
Map condition = new HashMap<>();
for (String t : texts)
condition.put(t, false);
for (Element elm : d.body().select("*"))
for (Node child : elm.childNodes()) {
if (child instanceof TextNode) {
TextNode tnode = ((TextNode) child);
String text = tnode.getWholeText();
for (String t : texts)
if (!condition.get(t) && text.contains(t)) {
condition.put(t, true);
boolean found = true;
for (String c : texts)
if (!condition.get(c)) {
found = false;
break;
}
if (found)
return true;
}
}
}
return false;
}
static SpannableStringBuilder fromDocument(
Context context, @NonNull Document document,
@Nullable ImageGetterEx imageGetter, @Nullable Html.TagHandler tagHandler) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean debug = prefs.getBoolean("debug", false);
boolean monospaced_pre = prefs.getBoolean("monospaced_pre", false);
final int colorPrimary = Helper.resolveColor(context, androidx.appcompat.R.attr.colorPrimary);
final int colorAccent = Helper.resolveColor(context, androidx.appcompat.R.attr.colorAccent);
final int colorBlockquote = Helper.resolveColor(context, R.attr.colorBlockquote, colorPrimary);
final int colorSeparator = Helper.resolveColor(context, R.attr.colorSeparator);
int bulletGap = context.getResources().getDimensionPixelSize(R.dimen.bullet_gap_size);
int bulletRadius = context.getResources().getDimensionPixelSize(R.dimen.bullet_radius_size);
int bulletIndent = context.getResources().getDimensionPixelSize(R.dimen.bullet_indent_size);
int intentSize = context.getResources().getDimensionPixelSize(R.dimen.indent_size);
int quoteGap = context.getResources().getDimensionPixelSize(R.dimen.quote_gap_size);
int quoteStripe = context.getResources().getDimensionPixelSize(R.dimen.quote_stripe_width);
int line_dash_length = context.getResources().getDimensionPixelSize(R.dimen.line_dash_length);
int message_zoom = prefs.getInt("message_zoom", 100);
float textSize = Helper.getTextSize(context, 0) * message_zoom / 100f;
// https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
NodeTraversor.traverse(new NodeVisitor() {
private Element element;
private int plain = 0;
private List block = new ArrayList<>();
private final Pattern FOLD_WHITESPACE = Pattern.compile("[ \t\f\r\n]+");
// https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
@Override
public void head(Node node, int depth) {
if (node instanceof TextNode) {
if (plain == 0)
block.add((TextNode) node);
} else if (node instanceof Element) {
element = (Element) node;
if ("true".equals(element.attr("x-plain")))
plain++;
if (element.isBlock() /* x-block is never false */ ||
"true".equals(element.attr("x-block"))) {
normalizeText(block);
block.clear();
}
}
}
@Override
public void tail(Node node, int depth) {
if (node instanceof Element) {
element = (Element) node;
if ("true".equals(element.attr("x-plain")))
plain--;
if (element.isBlock() /* x-block is never false */ ||
"true".equals(element.attr("x-block")) ||
"br".equals(element.tagName())) {
normalizeText(block);
block.clear();
}
}
}
private void normalizeText(List block) {
// https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace
TextNode tnode;
String text;
for (int i = 0; i < block.size(); ) {
tnode = block.get(i);
text = tnode.getWholeText();
if ("-- ".equals(text)) {
tnode.text(text);
i++;
continue;
}
// Fold white space
text = FOLD_WHITESPACE.matcher(text).replaceAll(" ");
// Conditionally remove leading whitespace
if (isSpace(text, 0) &&
(i == 0 || endsWithSpace(block.get(i - 1).text())))
text = text.substring(1);
// Soft hyphen
if (text.trim().equals("\u00ad"))
text = "";
tnode.text(text);
if (TextUtils.isEmpty(text))
block.remove(i);
else
i++;
}
// Remove trailing whitespace
while (block.size() > 0) {
tnode = block.get(block.size() - 1);
text = tnode.getWholeText();
if (endsWithSpace(text) && !"-- ".equals(text)) {
text = text.substring(0, text.length() - 1);
tnode.text(text);
}
if (TextUtils.isEmpty(text))
block.remove(block.size() - 1);
else
break;
}
// Remove all blank blocks
boolean blank = true;
for (int i = 0; i < block.size(); i++) {
text = block.get(i).getWholeText();
for (int j = 0; j < text.length(); j++) {
char kar = text.charAt(j);
if (kar != ' ') {
blank = false;
break;
}
}
}
if (blank)
for (int i = 0; i < block.size(); i++)
block.get(i).text("");
if (debug) {
if (block.size() > 0) {
TextNode first = block.get(0);
TextNode last = block.get(block.size() - 1);
first.text("(" + first.getWholeText());
last.text(last.getWholeText() + ")");
}
}
}
boolean isSpace(String text, int index) {
if (index < 0 || index >= text.length())
return false;
return (text.charAt(index) == ' ');
}
boolean endsWithSpace(String text) {
return isSpace(text, text.length() - 1);
}
}, document.body());
// https://developer.android.com/guide/topics/text/spans
SpannableStringBuilder ssb = new SpannableStringBuilderEx();
NodeTraversor.traverse(new NodeVisitor() {
private Element element;
private TextNode tnode;
private Typeface wingdings = null;
@Override
public void head(Node node, int depth) {
if (node instanceof Element) {
element = (Element) node;
Element prev = element.previousElementSibling();
if ("true".equals(element.attr("x-block")))
if (ssb.length() > 0 && ssb.charAt(ssb.length() - 1) != '\n')
ssb.append('\n');
if ("true".equals(element.attr("x-paragraph")) &&
!"false".equals(element.attr("x-line-before")))
if (ssb.length() > 1 &&
(ssb.charAt(ssb.length() - 2) != '\n' ||
ssb.charAt(ssb.length() - 1) != '\n'))
ssb.append('\n');
if ("true".equals(element.attr("x-line-before")) &&
!"true".equals(element.attr("x-paragraph")) &&
(prev == null || !"true".equals(prev.attr("x-line-after"))) &&
ssb.length() > 0 && ssb.charAt(ssb.length() - 1) == '\n')
ssb.append('\n');
element.attr("start-index", Integer.toString(ssb.length()));
if (debug)
ssb.append("[" + element.tagName() + "/" + element.className() +
":" + "bl=" + element.attr("x-block") +
":" + "pa=" + element.attr("x-paragraph") +
":" + "fo=" + element.attr("x-line-before") +
":" + "af=" + element.attr("x-line-after") +
":" + element.attr("style") + "]");
} else if (node instanceof TextNode) {
tnode = (TextNode) node;
String text = tnode.getWholeText();
ssb.append(text);
}
}
@Override
public void tail(Node node, int depth) {
if (node instanceof Element) {
element = (Element) node;
int start = Integer.parseInt(element.attr("start-index"));
// Apply style
String style = element.attr("style");
if (!TextUtils.isEmpty(style)) {
String[] params = style.split(";");
for (String param : params) {
int semi = param.indexOf(":");
if (semi < 0)
continue;
String key = param.substring(0, semi);
String value = param.substring(semi + 1);
switch (key) {
case "color":
case "background":
case "background-color":
if (!TextUtils.isEmpty(value))
try {
int color = parseWebColor(value);
CharacterStyle span;
if ("color".equals(key))
span = new ForegroundColorSpan(color);
else
span = new BackgroundColorSpan(color);
setSpan(ssb, span, start, ssb.length());
} catch (Throwable ex) {
Log.i(ex);
}
break;
case "font-weight":
Integer fweight = getFontWeight(value);
if (fweight != null)
setSpan(ssb, new StyleSpan(fweight >= 600 ? Typeface.BOLD : Typeface.NORMAL), start, ssb.length());
break;
case "font-family":
if ("wingdings".equalsIgnoreCase(value)) {
if (wingdings == null)
wingdings = ResourcesCompat.getFont(context.getApplicationContext(), R.font.wingdings);
int from = start;
for (int i = start; i < ssb.length(); i++) {
int kar = ssb.charAt(i);
if (MAP_WINGDINGS.containsKey(kar)) {
if (from < i) {
TypefaceSpan span = new CustomTypefaceSpan("wingdings", wingdings);
setSpan(ssb, span, from, i);
}
int codepoint = MAP_WINGDINGS.get(kar);
String replacement = new String(Character.toChars(codepoint));
ssb.replace(i, i + 1, replacement);
i += replacement.length() - 1;
from = i + 1;
}
}
if (from < ssb.length()) {
TypefaceSpan span = new CustomTypefaceSpan("wingdings", wingdings);
setSpan(ssb, span, from, ssb.length());
}
} else
setSpan(ssb, StyleHelper.getTypefaceSpan(value, context), start, ssb.length());
break;
case "font-style":
if ("italic".equals(value))
setSpan(ssb, new StyleSpan(Typeface.ITALIC), start, ssb.length());
break;
case "text-decoration":
if ("line-through".equals(value))
setSpan(ssb, new StrikethroughSpan(), start, ssb.length());
else if ("underline".equals(value))
setSpan(ssb, new UnderlineSpan(), start, ssb.length());
break;
case "text-align":
// https://developer.mozilla.org/en-US/docs/Web/CSS/text-align
Layout.Alignment alignment = null;
boolean rtl;
try {
rtl = TextDirectionHeuristics.FIRSTSTRONG_LTR.isRtl(ssb, start, ssb.length() - start);
} catch (Throwable ex) {
// IllegalArgumentException
Log.e(ex);
rtl = false;
}
switch (value) {
case "left":
case "start":
alignment = (rtl ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_NORMAL);
break;
case "center":
alignment = Layout.Alignment.ALIGN_CENTER;
break;
case "right":
case "end":
alignment = (rtl ? Layout.Alignment.ALIGN_NORMAL : Layout.Alignment.ALIGN_OPPOSITE);
break;
case "justify":
// Not supported by Android
break;
}
if (alignment != null)
setSpan(ssb, new AlignmentSpan.Standard(alignment), start, ssb.length());
break;
case "visibility":
if ("hidden".equals(value)) {
for (ForegroundColorSpan span : ssb.getSpans(start, ssb.length(), ForegroundColorSpan.class))
ssb.removeSpan(span);
for (BackgroundColorSpan span : ssb.getSpans(start, ssb.length(), BackgroundColorSpan.class))
ssb.removeSpan(span);
setSpan(ssb, new ForegroundColorSpan(Color.TRANSPARENT), start, ssb.length());
}
break;
}
}
}
// Apply calculated font size
String xFontSizeAbs = element.attr("x-font-size-abs");
if (TextUtils.isEmpty(xFontSizeAbs)) {
String xFontSizeRel = element.attr("x-font-size-rel");
if (!TextUtils.isEmpty(xFontSizeRel)) {
float fsize = Float.parseFloat(xFontSizeRel);
if (fsize != 1.0f)
setSpan(ssb, new RelativeSizeSpan(fsize), start, ssb.length());
}
} else {
int px = Integer.parseInt(xFontSizeAbs);
setSpan(ssb, new AbsoluteSizeSpan(px), start, ssb.length());
}
// Apply element
try {
String tag = element.tagName();
int semi = tag.indexOf(':');
if (semi >= 0)
tag = tag.substring(semi + 1);
switch (tag) {
case "a":
String href = element.attr("href");
if (!TextUtils.isEmpty(href)) {
if (false && BuildConfig.DEBUG) {
Uri uri = UriHelper.guessScheme(Uri.parse(href));
if (UriHelper.isHyperLink(uri))
ssb.append("\uD83D\uDD17"); // 🔗
// Unicode 6.0, supported since Android 4.1
// https://developer.android.com/guide/topics/resources/internationalization
}
setSpan(ssb, new URLSpan(href), start, ssb.length());
}
break;
case "big":
setSpan(ssb, new RelativeSizeSpan(FONT_LARGE), start, ssb.length());
break;
case "blockquote":
if (start == 0 || ssb.charAt(start - 1) != '\n')
ssb.insert(start++, "\n");
if (start == ssb.length())
ssb.append(' ');
if (ssb.length() == 0 || ssb.charAt(ssb.length() - 1) != '\n')
ssb.append("\n");
if (hasBorder(element)) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
setSpan(ssb, new QuoteSpan(colorBlockquote), start, ssb.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
else
setSpan(ssb, new QuoteSpan(colorBlockquote, quoteStripe, quoteGap), start, ssb.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
} else
setSpan(ssb, new IndentSpan(intentSize), start, ssb.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
break;
case "br":
ssb.append('\n');
int l = ssb.length() - 1;
List